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
- Variables
- func APIRoutes() []apidoc.Route
- func AnnounceForwarderRevocation(db *statedb.DB, origin, forwarderID, bindingID, reason string) error
- func AnnounceMembershipChange(db *statedb.DB, origin, projectPath string) error
- func AnnounceSessionsEnded(db *statedb.DB, origin string, sessionIDs []string, all bool) error
- func AnnounceStaticTokenRetired(db *statedb.DB, origin, fingerprint string) error
- func AnnounceTokensRevoked(db *statedb.DB, origin string, tokenIDs []string) error
- func ClaudeHomesOnMembers(db *statedb.DB, origin string, local offboard.ClaudeHomes) offboard.ClaudeHomes
- func ClaudeHomesRoot() string
- func CreateForwarderBinding(db *statedb.DB, in ForwarderBindingInput, now time.Time) (statedb.ForwarderBindingRow, error)
- func ForwarderBindingInterface(target, address, gateway string, mtu int) forwardedInterface
- func NewReproduceRunner(exe string) taskreplay.SandboxRunner
- func OperatorProjectWarnings(workdir string, cfg *config.Config, paths []string) []string
- func RecordRenewOutcome(outcome oidcauth.RenewOutcome)
- func RevokeForwarderBindingRecord(db *statedb.DB, bindingID, actor, reason string, now time.Time) (statedb.ForwarderBindingRow, bool, error)
- func ScrubRetiredKMSKeys(db *statedb.DB, prefix string)
- func StartAuditFailureStatus(workDir string) (adopted int64)
- func TokenStatus(t *apitoken.Token, now time.Time) string
- type ChatMessage
- type ExecutorRevocationAnnouncement
- type ExecutorRevocationMemberReport
- type ExecutorRevocationReport
- type ForwarderBindingInput
- type GrantRevocationAnnouncement
- type GrantRevocationMemberReport
- type GrantRevocationReport
- type KMSPoll
- type LeaseRevocationState
- type LeaseTakeBack
- type Server
- func (s *Server) BoundAddr() net.Addr
- func (s *Server) EffectiveRoleFor(identity string, groups, roles []string) (string, int)
- func (s *Server) Handler() http.Handler
- func (s *Server) InstallOIDCHooks(cfg *oidcauth.Config)
- func (s *Server) KMSHealth() map[string]KMSPoll
- func (s *Server) OpenMemberStore() (*projectmember.Store, error)
- func (s *Server) OpenRoleStore() (*rolestore.Store, error)
- func (s *Server) OpenSCIM(cfg config.SCIMConfig) (*provision.Directory, error)
- func (s *Server) OpenSessionStore() (oidcauth.SessionStore, error)
- func (s *Server) PreflightIdP(ctx context.Context) error
- func (s *Server) ReconcileQuotas()
- func (s *Server) RecordQuotaSpend(identity string, tokens, usd float64)
- func (s *Server) RequestIsTLS(r *http.Request) bool
- 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) SetKMS(providers []kms.Provider)
- 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 ( ExecutorOpCordon = "cordon" ExecutorOpDrain = "drain" ExecutorOpRevoke = "revoke" )
The changes to an executor the CLI announces.
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 )
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.
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.
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 APIRoutes ¶ added in v0.0.2
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
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
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
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
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
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
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
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
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.
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 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 ¶
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
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
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
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 ¶
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
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) 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
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
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
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 ¶
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
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 ¶
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) SetKMS ¶ added in v0.0.7
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 ¶
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, 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 ¶
TLSEnabled reports whether this process is configured to terminate TLS.
Source Files
¶
- adopt_api.go
- approval_notify.go
- approvals_api.go
- attach_api.go
- attachments_api.go
- audit_api.go
- audit_checkpoints.go
- audit_forward.go
- authz.go
- autobackup.go
- autoresume.go
- ci.go
- ci_api.go
- ci_sessions.go
- claimfreshness.go
- claude_identity.go
- claude_sweep.go
- cluster.go
- cluster_api.go
- cluster_handles.go
- cluster_leases.go
- cluster_misc.go
- cluster_oidc.go
- cluster_presence.go
- cluster_proxies.go
- cluster_runs.go
- config_lock.go
- cost_api.go
- csp.go
- cssstrip.go
- denials.go
- denials_api.go
- device_api.go
- device_forwarders.go
- device_forwarders_api.go
- dictate_api.go
- diskfloor.go
- dispatch_intent.go
- egress_dispatch.go
- egressproxy.go
- executor.go
- executor_agents.go
- executor_autoupdate.go
- executor_detail_api.go
- executor_labels.go
- executor_policy_api.go
- executor_revocation.go
- executor_sandbox.go
- executor_sandbox_api.go
- executor_supervisor.go
- executoraudience.go
- executors_api.go
- executors_edge.go
- executors_inventory.go
- executors_upgrade_api.go
- failover_runs.go
- failover_settle.go
- features.go
- features_api.go
- features_isolated.go
- firewall_api.go
- firewall_dispatch.go
- forwarder_bindings.go
- forwarder_cluster.go
- forwarder_dispatch.go
- gitguard.go
- githubapp_api.go
- gitproxy.go
- gitproxy_pushscan.go
- glasses_api.go
- grant_revoke.go
- gzip_api.go
- harness.go
- harness_credential.go
- harness_credential_api.go
- history.go
- htmlstrip.go
- hubca_api.go
- hubconfig.go
- hubhealth.go
- hubmetrics.go
- hubtls.go
- icons.go
- install_script.go
- isolation.go
- jsstrip.go
- kms.go
- kubeguard.go
- kubeshare.go
- lease_revocation.go
- ledger_api.go
- listen.go
- livelog.go
- members.go
- members_api.go
- model_calls.go
- model_relay.go
- model_relay_api.go
- model_relay_dispatch.go
- model_relay_lease.go
- model_relay_spend.go
- notifications_config_api.go
- offboard_api.go
- offboard_claude.go
- oidc.go
- oidc_config_api.go
- oidcconfig.go
- openapi_api.go
- originguard.go
- project_confine.go
- projects_root.go
- provider_calls.go
- provision.go
- proxy_session_store.go
- pushscan_open_unix.go
- quotas_api.go
- reproduce_api.go
- reproduce_runner.go
- request_approvers.go
- requests_api.go
- resourcelimits.go
- resume_account.go
- retention.go
- retention_api.go
- reviewgate_api.go
- revise_api.go
- roles.go
- routes.go
- run_progress.go
- run_result.go
- run_stop.go
- run_takeover.go
- sandbox.go
- scim_admin_api.go
- scim_api.go
- secrets.go
- secrets_api.go
- secrets_owner.go
- secrets_refresh.go
- secrets_revoke.go
- secrets_sweep.go
- server.go
- session_hold.go
- session_janitor.go
- session_restore.go
- sessions_api.go
- spend.go
- stale_recovery.go
- state_diff.go
- statedb_wal.go
- static.go
- static_token.go
- static_token_api.go
- stream_credentials.go
- subprocess.go
- suggest_api.go
- telemetry_api.go
- telemetry_config_api.go
- tls.go
- tokens_api.go
- unguarded_api.go
- validate.go
- version_api.go
- virtual_executors.go
- virtual_executors_api.go
- workspace.go
- workspace_lease.go
- writeback_push.go