ui

package
v0.0.7 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 146 Imported by: 0

Documentation

Overview

Package ui - claude_identity.go resolves which Claude Code account a request acts on, and hands the answer to both the auth endpoints and the run dispatcher.

Motivation (Task 20241): before this, a hub had exactly one Claude login. Whoever logged in last owned it; every user's tasks ran on that person's subscription; and any user clicking "Log out" signed out the whole deployment. With OIDC enabled each signed-in user now gets their own credential, session history and usage figures, stored in a private configuration directory (see pkg/claudecodeauth/identity.go).

Deployments without OIDC are untouched: one machine, one operator, one credential in the host's own ~/.claude, exactly as before.

harness.go resolves which agent CLI a project's workload will need, so that placement can refuse an executor that does not have it.

The value travels on executor.Spec.Harness and is consumed by pkg/executor/remote's checkHarness, whose file comment carries the reasoning about why the device's inventory is the right thing to check in host mode and the wrong thing in container mode. What belongs *here* is only the mapping from a project to a binary name, and that mapping lives in the UI for a structural reason: the provider is a property of the project's config and state, neither of which pkg/executor may import — it is the layer both of them sit above.

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

Index

Constants

View Source
const (
	ExecutorOpCordon = "cordon"
	ExecutorOpDrain  = "drain"
	ExecutorOpRevoke = "revoke"
)

The changes to an executor the CLI announces.

View Source
const (
	// ForwarderBindingTTLDefault is a binding's lifetime when none is asked
	// for.
	ForwarderBindingTTLDefault = 24 * time.Hour
	// ForwarderBindingTTLMax bounds any binding: hardware access is a grant,
	// and grants end.
	ForwarderBindingTTLMax = 90 * 24 * time.Hour
)
View Source
const AgentUpgradeCommand = executor.AgentUpgradeProcedure

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.

An alias rather than a second literal: the scheduler emits the same sentence when it refuses a device for being below the fleet's build floor, and two copies of an operator instruction is how one of them goes stale.

View Source
const AuditSpendRefused = auditaction.ActionQuotaSpendRefused

AuditSpendRefused is the audit event type for a run stopped because its paying identity exhausted a daily budget. Named rather than inlined so the Audit panel's filter and any SIEM rule key off one constant.

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 APIRoutes added in v0.0.2

func APIRoutes() []apidoc.Route

APIRoutes describes the hub's HTTP surface.

Exported for the documentation generator in tests/docs, which renders docs/reference/http-api.md from this and from pkg/apiserver.APIRoutes so the published reference is generated from the route tables rather than maintained alongside them.

It builds a zero Server on purpose: the table's handlers are method values, which are well-formed without any server state, and the description depends on nothing else. A documentation generator must not need a database, a project registry, or a listening port.

func AnnounceForwarderRevocation added in v0.0.6

func AnnounceForwarderRevocation(db *statedb.DB, origin, forwarderID, bindingID, reason string) error

AnnounceForwarderRevocation is the CLI's announcement of a forwarder or binding revoked in db: `cloop forward revoke` and `cloop forward unbind`, which are no hub member. Every member cuts the session and the relays it holds within a bus poll; one that missed the notice does it within its own re-check of the database (deviceforward.DefaultRevocationRecheck for the session, forwardRecheckInterval for a run's device).

func AnnounceMembershipChange added in v0.0.5

func AnnounceMembershipChange(db *statedb.DB, origin, projectPath string) error

AnnounceMembershipChange posts the bus event a hub member would publish after changing project_members, for a writer that is not a member — `cloop project members`. Every live member polls the bus and reloads within its poll interval; a member that is down reads the table fresh when it starts. A failure is returned for the caller to report, never fatal to the write: without the event the change still lands within the hubs' refresh interval.

func AnnounceSessionsEnded added in v0.0.5

func AnnounceSessionsEnded(db *statedb.DB, origin string, sessionIDs []string, all bool) error

AnnounceSessionsEnded posts the bus event a hub member publishes when a session ends, for a writer that is not a member: `cloop hub session revoke` and `cloop hub user offboard`, which delete session rows directly. Every member reading the bus drops the sessions from its cache and closes the streams and terminals they opened within its poll interval. all names every session at once, for a revocation of all of them.

A failure is returned for the caller to report, never fatal to the revocation, which has already happened: a member that misses the event stops honouring the session once its cache ages out, and closes its streams at their next keepalive re-check after that.

func AnnounceStaticTokenRetired added in v0.0.5

func AnnounceStaticTokenRetired(db *statedb.DB, origin, fingerprint string) error

AnnounceStaticTokenRetired posts the bus notice a hub member publishes after retiring a static token, for a writer that is not a member: `cloop hub token static retire`. Every member reading the bus re-reads the retired set and closes the streams and terminals the token opened there within its poll interval. A failure is the caller's to report, never fatal to the retirement, which has already happened: a member that misses the notice refuses the token at its next re-read, within ReportInterval.

func AnnounceTokensRevoked added in v0.0.5

func AnnounceTokensRevoked(db *statedb.DB, origin string, tokenIDs []string) error

AnnounceTokensRevoked is AnnounceSessionsEnded for API tokens and glasses links revoked by a writer that is not a hub member — `cloop hub token revoke` and `cloop hub user offboard`. Without it a member still closes the streams at their next keepalive re-check, which reads the token row.

func ClaudeHomesOnMembers added in v0.0.5

func ClaudeHomesOnMembers(db *statedb.DB, origin string, local offboard.ClaudeHomes) offboard.ClaudeHomes

ClaudeHomesOnMembers is the Claude login surface of `cloop hub user offboard`: its own tree, through local, and every running member's, asked on the bus (Task 20400). A member that does not answer within the wait is reported unreached; so is a hub holding the control plane exclusively, which reads no bus, and a stopped member whose tree on a host that is still up nobody reached.

func ClaudeHomesRoot added in v0.0.5

func ClaudeHomesRoot() string

ClaudeHomesRoot is where this process keeps per-user Claude homes, for the member row (cmd/ui_cluster.go). Empty when the process has no config directory to resolve one from.

func CreateForwarderBinding added in v0.0.6

func CreateForwarderBinding(db *statedb.DB, in ForwarderBindingInput, now time.Time) (statedb.ForwarderBindingRow, error)

CreateForwarderBinding validates and records a binding, and audits it as forwarder.bind. It is the one path every surface makes a binding through — the dashboard, an approved request and `cloop forward bind` — so the rules cannot drift between them. The caller has checked who may.

func ForwarderBindingInterface added in v0.0.6

func ForwarderBindingInterface(target, address, gateway string, mtu int) forwardedInterface

ForwarderBindingInterface builds the L2 shape of a binding, for callers outside this package (the CLI).

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 OperatorProjectWarnings added in v0.0.5

func OperatorProjectWarnings(workdir string, cfg *config.Config, paths []string) []string

OperatorProjectWarnings checks the directories an operator named on `cloop ui`'s command line (--projects, --scan) against the invariant, beside the projects already registered, and returns one warning per directory that breaks it. They are registered anyway — the operator owns the host and may mean it — and then flagged on the Projects tab and by `cloop hub doctor`, and refused at every dispatch until they are moved (Task 20418).

func RecordRenewOutcome added in v0.0.5

func RecordRenewOutcome(outcome oidcauth.RenewOutcome)

RecordRenewOutcome counts one silent renewal that reached a verdict. Wired into the authenticator as Config.RenewObserver by cmd/ui_cmd.go; a function rather than a method because it reads nothing from a server.

func RevokeForwarderBindingRecord added in v0.0.6

func RevokeForwarderBindingRecord(db *statedb.DB, bindingID, actor, reason string, now time.Time) (statedb.ForwarderBindingRow, bool, error)

RevokeForwarderBindingRecord revokes a binding in db and audits it as forwarder.unbind. It reports whether this call revoked it. Cutting the live relays is the caller's: a hub member announces it, the CLI announces it on the bus (AnnounceForwarderRevocation).

func ScrubRetiredKMSKeys added in v0.0.7

func ScrubRetiredKMSKeys(db *statedb.DB, prefix string)

ScrubRetiredKMSKeys destroys the KMS ciphertext retired keys still carry (statedb.ScrubRetiredKEKs), saying so on stderr under prefix. A build from before Task 20433 leaves it behind when it retires a KMS-held key, so `cloop ui` runs this at start — with or without a KMS now, since the key may have been retired part-way through a move off one — and the leader at each poll.

func StartAuditFailureStatus added in v0.0.5

func StartAuditFailureStatus(workDir string) (adopted int64)

StartAuditFailureStatus points this process's audit failure accounting at workDir's status directory, and records, as gap rows in their chains, the losses processes that died there left behind. Called by `cloop ui`, not by Run: the accounting is per process, and a test that builds a Server must not repoint it.

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 ExecutorRevocationAnnouncement added in v0.0.6

type ExecutorRevocationAnnouncement struct {
	ExecutorID string
	// Op is ExecutorOpCordon, ExecutorOpDrain or ExecutorOpRevoke.
	Op     string
	Reason string
	Actor  string
}

ExecutorRevocationAnnouncement is what the CLI tells running hubs about a change it wrote to the store.

type ExecutorRevocationMemberReport added in v0.0.6

type ExecutorRevocationMemberReport struct {
	Member   string `json:"member"`
	Hostname string `json:"hostname,omitempty"`
	// Refused says why the member did nothing: the store does not record the
	// change the announcement claims.
	Refused string `json:"refused,omitempty"`
	// HoldsSession reports that the device's session is on this member.
	HoldsSession bool `json:"holds_session,omitempty"`
	// Acted reports that this member took the device's leases back: it holds
	// the session, or no live member does and it leads adoption.
	Acted bool `json:"acted,omitempty"`
	// Disconnected reports, for a revoked agent, that this member dropped the
	// session it held.
	Disconnected bool `json:"disconnected,omitempty"`
	// Leases is what each lease came to on this member.
	Leases []LeaseTakeBack `json:"leases,omitempty"`
}

ExecutorRevocationMemberReport is one member's answer.

type ExecutorRevocationReport added in v0.0.6

type ExecutorRevocationReport struct {
	// Device is the edge agent whose session serves the executor: the
	// executor itself, or the device a virtual executor runs through. Empty
	// for an executor on the hub itself, whose change takes nothing back and
	// is not announced.
	Device string
	// Recorded are the leases recorded as issued to it, recorded revoked
	// before the announcement.
	Recorded []string
	// Answered are the members' answers; Silent the members alive when it was
	// announced that did not answer within the wait.
	Answered []ExecutorRevocationMemberReport
	Silent   []string
	// Exclusive names a hub holding the control plane alone, which reads no
	// bus; ExclusiveDetail says what it does instead.
	Exclusive       string
	ExclusiveDetail string
	// NoHub: no hub serves this control plane, and the recorded revocations
	// wait for the next to start.
	NoHub bool
	// Leases is what the store records of each recorded lease afterwards.
	Leases []LeaseRevocationState
}

ExecutorRevocationReport is what an announced executor change came to.

func AnnounceExecutorRevocation added in v0.0.6

func AnnounceExecutorRevocation(db *statedb.DB, origin string, ann ExecutorRevocationAnnouncement,
	wait time.Duration) (ExecutorRevocationReport, error)

AnnounceExecutorRevocation tells every running hub member about a cordon, a drain or an agent revocation the caller has already written to the store, records revoked the leases the device holds, waits up to wait for the members alive at the time to answer, and reports what they did and what the store then records of each lease — for `cloop executor cordon|drain|revoke`, which change the store from a process that reaches no device.

An executor that serves no edge device is not announced: there is nothing on a device to take back.

type ForwarderBindingInput added in v0.0.6

type ForwarderBindingInput struct {
	ForwarderID     string
	Project         string
	VirtualExecutor string
	Interface       forwardedInterface
	TTL             time.Duration
	Note            string
	Actor           string
	RequestID       string
}

ForwarderBindingInput is one binding to make.

type GrantRevocationAnnouncement added in v0.0.5

type GrantRevocationAnnouncement struct {
	GrantID      string
	Actor        string
	Superseded   bool
	SupersededBy string
}

GrantRevocationAnnouncement is what a process that is no hub member — the CLI — tells running hubs about a grant it revoked.

type GrantRevocationMemberReport added in v0.0.5

type GrantRevocationMemberReport struct {
	Member   string
	Hostname string
	// Leases is how many leases gave the grant back there, KeptLeases how many
	// a supersession left standing on the successor.
	Leases     int
	KeptLeases int
	// State is the weakest holder state across them.
	State remote.RevokeState
	// Holders summarises each holder's outcome, one line each.
	Holders []string
}

GrantRevocationMemberReport is one member's answer.

type GrantRevocationReport added in v0.0.5

type GrantRevocationReport struct {
	// Answered are the members that took the grant back from what they hold,
	// with what each did.
	Answered []GrantRevocationMemberReport
	// Silent are the members alive when it was announced that did not answer
	// within the wait.
	Silent []string
	// Exclusive names a hub holding the control plane alone, which reads no
	// bus; Detail says when it finds the revocation anyway.
	Exclusive       string
	ExclusiveDetail string
	// NoHub: no hub serves this control plane, so no workload holds a lease
	// from it.
	NoHub bool
}

GrantRevocationReport is what the running hubs answered.

func AnnounceGrantRevoked added in v0.0.5

func AnnounceGrantRevoked(db *statedb.DB, origin string, ann GrantRevocationAnnouncement, wait time.Duration) (GrantRevocationReport, error)

AnnounceGrantRevoked tells every running hub member that a grant was revoked, waits up to wait for each member alive at the time to answer, and reports what they did — for `cloop secret revoke`, which revokes from a process that holds no lease and can reach no workload (Task 20403). The grant is already revoked in the store: a failure here leaves the material in the workloads until the hub's lease janitor finds the revocation, and is reported as such, not as a failure to revoke.

func AnnounceGrantsRevoked added in v0.0.5

func AnnounceGrantsRevoked(db *statedb.DB, origin string, anns []GrantRevocationAnnouncement, wait time.Duration) (GrantRevocationReport, error)

AnnounceGrantsRevoked is AnnounceGrantRevoked for several grants at once — `cloop hub user offboard` revokes every grant over a departed person's secrets — with one wait for all of them.

type KMSPoll added in v0.0.7

type KMSPoll struct {
	Status kms.Status
	Err    string
	At     time.Time
}

KMSPoll is one provider's last status poll.

type LeaseRevocationState added in v0.0.6

type LeaseRevocationState struct {
	LeaseID string
	// Pending: not yet taken back from every holder; Detail says what it
	// waits for.
	Pending    bool
	Detail     string
	EnforcedBy string
}

LeaseRevocationState is what the store records of one revoked lease once the members have answered.

type LeaseTakeBack added in v0.0.6

type LeaseTakeBack struct {
	LeaseID string             `json:"lease_id"`
	State   remote.RevokeState `json:"state"`
	Detail  string             `json:"detail,omitempty"`
}

LeaseTakeBack is what one lease came to on the member that took it back.

type Server

type Server struct {
	WorkDir  string
	Port     int
	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 websocket.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 origin is one of the hub's own for the
	// dashboard, the executor-agent endpoint and the forgery guard, and its
	// host one an open hub answers to (Task 20394). Populated from
	// config.UIConfig.ExternalURL.
	ExternalURL string

	// TrustedProxies is ui.trusted_proxies: the peers whose
	// X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-For are believed,
	// loopback always among them (Task 20394). The zero value is loopback
	// only — a proxy on this machine — which is what the hub trusted before
	// the setting existed, minus the X-Forwarded-Proto it used to take from
	// anyone once ui.external_url was https.
	TrustedProxies sameorigin.Proxies

	// AllowedHosts is ui.allowed_hosts: host names a hub with no sign-in
	// answers to besides localhost, IP addresses, ui.external_url's host and
	// the cluster's advertise hosts. Any other Host is refused (hostGuard).
	AllowedHosts []string

	// ListenHost is the address Run binds, without the port: --listen, else
	// ui.listen. Empty takes the default for this hub's authentication —
	// every interface with OIDC or a Token, 127.0.0.1 without either — and
	// an address beyond loopback on a hub with neither is refused unless
	// AllowUnauthenticatedNetwork is set (Task 20393; pkg/exposure). Run
	// decides before it starts anything, so a refused start leaves nothing
	// running.
	ListenHost string
	// AllowUnauthenticatedNetwork is ui.allow_unauthenticated_network: a hub
	// with no browser credential may bind the address ListenHost names even
	// beyond loopback, and warns at every start that it does.
	AllowUnauthenticatedNetwork bool

	// 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

	// RequireIdP makes a failed identity-provider preflight fatal at startup
	// instead of a warning (ui.oidc.require_idp / --require-idp, Task 20247).
	// Only consulted by PreflightIdP; readiness reports a degraded identity
	// path either way.
	RequireIdP bool

	// 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

	// Cluster is this hub's membership of a control plane served by several
	// `cloop ui` processes (Task 20354). Nil means standalone: this process
	// leads, owns everything and publishes nothing, which is also what every
	// test that builds a Server as a struct literal gets. Set by `cloop ui`
	// before New, through NewInCluster, because bootstrap already needs to
	// know whether peers are serving — a startup sweep that assumed it was
	// alone would reap their workloads. See cluster.go.
	Cluster *hubcluster.Node
	// 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 NewInCluster added in v0.0.5

func NewInCluster(workdir string, port int, token string, node *hubcluster.Node) *Server

NewInCluster is New for a hub process that is one of several serving the control plane at workdir (Task 20354). node must already have joined: the executor bootstrap below decides from it whether peers are serving, and a startup sweep that believed it was alone would reap their workloads and wipe their credentials. A nil node is New.

func (*Server) BoundAddr added in v0.0.5

func (s *Server) BoundAddr() net.Addr

BoundAddr is the address Run's listener holds, or nil before it has bound and after a refused start.

func (*Server) EffectiveRoleFor added in v0.0.2

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

EffectiveRoleFor resolves a set of claims to a role name and its rank on the role ladder, for the deprivileging hook in pkg/oidcauth (Task 20249).

Resolved live against s.Authz rather than captured, so a role mapping an operator changes — or a runtime binding written with `cloop hub role` during an incident — is reflected in the next revalidation without a restart. Global scope: the question being asked is "did this user's authority shrink", which is about the identity, not about one project.

Rank is the index in authz.AllRoles, which is ordered weakest to strongest. An unknown role reports -1 so it can never look like an upgrade.

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) InstallOIDCHooks added in v0.0.5

func (s *Server) InstallOIDCHooks(cfg *oidcauth.Config)

InstallOIDCHooks adds this hub's hooks to an authenticator config.

On every hub, a session that changes — revoked, signed out, expired, its claims refreshed — re-checks the streams and terminals this hub holds open with it, and closes them if it ended (Task 20398). On a cluster member the same notice also goes on the bus, and logins and refreshes get the cluster hooks described at the top of this file.

func (*Server) KMSHealth added in v0.0.7

func (s *Server) KMSHealth() map[string]KMSPoll

KMSHealth reports each provider's last poll, by name.

func (*Server) OpenMemberStore added in v0.0.5

func (s *Server) OpenMemberStore() (*projectmember.Store, error)

OpenMemberStore returns the membership store backed by the hub's own control-plane database, opening it on first call.

The error is a real failure and `cloop ui` aborts startup on it, as it does for the role store: a hub that cannot read this table would come up having silently un-shared every project, and the only symptom would be projects vanishing from colleagues' dashboards.

func (*Server) OpenRoleStore added in v0.0.2

func (s *Server) OpenRoleStore() (*rolestore.Store, error)

OpenRoleStore returns the runtime binding source backed by the hub's own control-plane database, opening it on first call.

Unlike OpenSessionStore the error is a real failure and the caller should abort startup. The asymmetry is deliberate. A missing session store costs durability — people stay signed in, they just get signed out by a restart — whereas a missing binding source costs *containment*: every deny an operator has written silently stops applying, and the hub comes up handing authority back to accounts somebody deliberately took it from. Starting wide open is not a degraded mode of that, it is the failure it was meant to prevent.

The database is the hub's, never a managed project's, for the sharpest version of the usual reason: a tenant able to write this table could grant itself admin across the fleet with one INSERT.

func (*Server) OpenSCIM added in v0.0.5

func (s *Server) OpenSCIM(cfg config.SCIMConfig) (*provision.Directory, error)

OpenSCIM opens the provisioning directory over the hub's control plane and returns it, for the resolver to read provisioned groups from at every decision. `cloop ui` calls it when single sign-on is on, before building the resolver; an error is fatal there, for the reason an unreadable role store is: a hub that came up without its directory would grant no provisioned group and know of no deactivation.

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) PreflightIdP added in v0.0.2

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

PreflightIdP resolves the identity provider before the listener binds.

Discovery is otherwise lazy, which means a hub pointed at an unreachable, misspelled, or wrongly-registered issuer starts green, reports healthy, and fails for the first human who tries to sign in — with an error page written in the provider's words about a value the provider was never shown. This turns that into one line in the hub's own log at the moment an operator is watching, naming the issuer and the HTTP status.

It returns an error only when the deployment declared the provider mandatory (RequireIdP). Otherwise a failure degrades readiness and nothing more: the lazy path is still the retry mechanism, so an IdP that is down for a minute recovers on its own, and a hub holding live sessions keeps serving them rather than refusing to start over an outage it will outlive.

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) RequestIsTLS added in v0.0.5

func (s *Server) RequestIsTLS(r *http.Request) bool

RequestIsTLS is requestIsTLS for the OIDC authenticator: cookie_secure "auto" follows the same rule about whose X-Forwarded-Proto to believe as HSTS and the origin checks do (oidcauth.Config.RequestIsTLS; wired by cmd/ui_cmd.go).

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) SetKMS added in v0.0.7

func (s *Server) SetKMS(providers []kms.Provider)

SetKMS gives the server the KMS providers `cloop ui` resolved, current root first. Nil or empty means the hub runs without a KMS.

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, and grants revoked without a cascade, off every holder this member reaches.

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.

Source Files

Jump to

Keyboard shortcuts

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