Documentation
¶
Overview ¶
Package web serves the pipeline UI: a read-and-operate view of what the runner has done and is doing, over the same sqlite store the CLI writes.
It is a second front end on the existing model, never a second model. Every page answers a question the store already holds the answer to — what jobs exist and how they depend on each other, what a run did step by step, why a step was skipped, what an agent actually said — and the two mutations it offers (enqueue a job, decide an approval) go through the same rows `steps web` and `steps approvals approve` use. Nothing here is a parallel execution path.
The server is single-user and binds loopback by default, where it asks for nothing: the thing it would authenticate against does not exist — this is the local runner's own UI, in the same trust domain as the terminal that started it. A deployment that is NOT that turns on HTTP Basic (see auth.go), which is the whole of the authentication here.
Index ¶
- Constants
- Variables
- func CheckAccess(access *config.Access) error
- func FormatBinaryBytes(n int64) string
- func FormatUSD(amount float64) string
- type Authorizer
- type BrokenPipeline
- type Capacity
- type Exporter
- type Identity
- type LocalUser
- type LoginRequest
- type LoginStatus
- type MCPCredential
- type MCPProbe
- type MCPState
- type Manager
- type Option
- func WithActivityLog(open func() (store.Reader, error), limit int) Option
- func WithAllowedHosts(hosts []string) Option
- func WithBasicAuth(username, password string) Option
- func WithCapacity(capacity Capacity) Option
- func WithExporter(exporter Exporter) Option
- func WithExternalURL(base string) Option
- func WithLocalUsers(users []LocalUser) Option
- func WithLoopbackHosts(names ...string) Option
- func WithRoles(roles Roles) Option
- func WithSessions(accounts store.Accounts) Option
- func WithSignInProviders(providers ...SignInProvider) Option
- func WithTracing(m echo.MiddlewareFunc) Option
- func WithVersion(version string) Option
- type Pipeline
- type PipelineConfig
- type PipelineSummary
- type PlacementView
- type Role
- type Roles
- type Runner
- type Server
- func (s *Server) Add(pipeline *Pipeline) error
- func (s *Server) Broken() []BrokenPipeline
- func (s *Server) BrokenReason(name string) (string, bool)
- func (s *Server) ClearBroken(name string)
- func (s *Server) Close() error
- func (s *Server) Handler() http.Handler
- func (s *Server) Lookup(slug string) *Pipeline
- func (s *Server) MarkBroken(broken BrokenPipeline)
- func (s *Server) Remove(slug string) *Pipeline
- func (s *Server) Served() []*Pipeline
- func (s *Server) SetManager(manager Manager)
- func (s *Server) Start(ctx context.Context, addr string) error
- type SetRequest
- type SetResult
- type SignInProvider
Constants ¶
const ( LoginPending = "pending" LoginAuthorized = "authorized" LoginFailed = "failed" )
Login states, which the CLI polls for.
const HealthPath = "/healthz"
HealthPath answers ok to anything that can reach the listener; a platform's health check sends no credentials, and liveness is all it can learn.
const MCPCallbackPath = "/mcp/callback"
MCPCallbackPath is where an authorization server redirects the browser to finish a login this daemon started; the CLI builds the redirect URI from it, so it is one constant.
Variables ¶
var ErrNoSuchPipeline = errors.New("no such pipeline")
ErrNoSuchPipeline is a verb naming a pipeline this daemon does not hold.
var ErrPlainPassword = errors.New("a local user's password must be a bcrypt hash — make one with `htpasswd -nbB <name> <password>`")
ErrPlainPassword is a local user given in the clear rather than hashed.
var ErrRefused = errors.New("the pipeline was refused")
ErrRefused is the sender's to fix — unparseable, or naming something this machine cannot supply — which is why it is not a 500.
var ErrRevisionMoved = errors.New("the pipeline's configuration changed since it was read")
ErrRevisionMoved is a compare-and-set the daemon refused: the configuration moved between the sender's diff and its set.
Functions ¶
func CheckAccess ¶
CheckAccess is whether a pipeline's access: rules compile, which is what a set is refused over when one cannot work.
func FormatBinaryBytes ¶
FormatBinaryBytes renders a disk or transfer size in BINARY units, deliberately unlike formatBytes.
That one is decimal so an agent's payload is comparable to the byte limits it is bounded by. This is about disks and wire transfers, which every tool a reader will cross-check against — the EC2 console, df, docker system df — reports in KiB/MiB/GiB.
func FormatUSD ¶
FormatUSD renders a dollar figure at the precision an agent run actually costs. Four decimals rather than two: a single cheap step lands in the fractions of a cent, and rounding those to $0.00 loses exactly the number the column exists to show — the same reading an unpriced run must never produce.
Exported because the terminal report (`steps runs cost`) prints the same figures from the same rows, and two spellings of "how much" would disagree on exactly the cheap runs where the difference is the whole answer.
Types ¶
type Authorizer ¶
type Authorizer interface {
StartLogin(pipeline *Pipeline, server string, req LoginRequest) (LoginStatus, error)
LoginStatus(pipeline *Pipeline, server string) (LoginStatus, bool)
// LoginCallback is the handler for the pending login that minted state, nil when none did.
LoginCallback(state string) http.Handler
// MCPState is what the token-holder knows about one server: its saved credential, and the last probe anybody asked for. Never a request of its own — the page calls this on every poll.
MCPState(pipeline *Pipeline, server string) MCPState
// StartProbe connects to one server in the background, recording the result for MCPState to report. Refuses what it cannot honestly probe; a probe already in flight is left alone rather than duplicated.
StartProbe(pipeline *Pipeline, server string) error
}
Authorizer runs logins and reports what a declared mcp server is worth. An interface for the reason Manager is one — depguard keeps internal/mcp out of this package — and optional: a manager that is not one answers 501 rather than pretending.
type BrokenPipeline ¶
BrokenPipeline is one the database holds that this daemon could not serve, and why.
type Capacity ¶
type Capacity interface {
RunCapacity() (limit, active int)
}
Capacity is the daemon-wide run limit (0 when unlimited) and how many builds hold it.
type Exporter ¶
type Exporter interface {
ServeExport(ctx context.Context, target *scheduler.Pipeline, export config.MCPExport, hooks scheduler.ExportHooks) (*scheduler.ExportServer, error)
}
Exporter builds a pipeline's exports; scheduler.LocalRunner is one. Absent on a read-only server, which serves no exports: calling a tool is acting, the thing read-only withholds.
type Identity ¶
type Identity struct {
// Provider is where the name came from: local, github, google.
Provider string
// UserID is the name within it: a local account, a GitHub login, a Google address.
UserID string
// ID is the provider's own id for the account, which a rename does not change: GitHub's numeric user id, Google's subject, and for a local account its name. A rule that must not follow a renamed GitHub login onto whoever registers it next names this rather than UserID.
ID string
Email string
Organizations []string
Groups []string
}
Identity is what a role's rule is written against.
type LocalUser ¶
LocalUser is one of the daemon's own accounts: a name and the bcrypt hash of its password, so the daemon never holds the password itself.
func ParseLocalUser ¶
ParseLocalUser reads one "name:hash" entry, the format htpasswd -B writes. The error never repeats the entry: a plain-text password is exactly what it refuses.
type LoginRequest ¶
type LoginRequest struct {
Base string `json:"base"`
// Return is where the browser is sent once the callback has answered, and it is a path on THIS daemon — refused otherwise (see redirectFor's sibling). Empty is a login a terminal is waiting on, which keeps the callback's plain "you can close this window": the CLI is the thing that reports the outcome there. A page sets it because the exchange finishes after the callback has answered, so the failure that matters most has nowhere else to land.
Return string `json:"return,omitempty"`
}
LoginRequest starts one. Base is the address the CLI reached this daemon on, userinfo already removed: it is proven to work, the browser finishing the flow sits beside that CLI, and it is what the redirect URI is built from — so a password left on it would be handed to the authorization server.
type LoginStatus ¶
type LoginStatus struct {
State string `json:"state"`
// ID names THIS attempt, because a login is tracked per pipeline and server and a second Connect replaces the first under that pair. Without it a request can only ask "what is the state of this pipeline's login called tracker", which after a replacement is somebody else's.
ID string `json:"id,omitempty"`
// AuthorizeURL appears once discovery and registration are done, which is network work the start request does not wait out.
AuthorizeURL string `json:"authorize_url,omitempty"`
// Message is the refusal, in the flow's own words — including the one that matters most unattended: authorized, but with a token that cannot be renewed.
Message string `json:"message,omitempty"`
}
LoginStatus is where one login stands. Shared with the client for the reason SetRequest is.
type MCPCredential ¶
type MCPCredential struct {
Connected bool
// Detail is the sentence a reader acts on — "connected, renews automatically", "authorized for a different endpoint".
Detail string
}
MCPCredential is what the daemon knows about an oauth server's saved token without spending it. It carries no part of the token: a page renders this.
type MCPProbe ¶
type MCPProbe struct {
// Running is a probe still in flight, which the page's own poll will replace with its result.
Running bool
OK bool
Detail string
At time.Time
// Tools is what the server answered with, which is the question behind the question: a grant names a tool, and whether that name still exists is what a reader is really checking.
Tools []string
}
MCPProbe is the outcome of a Test: a live connection to the server, made because somebody asked for one and never on a page load.
type MCPState ¶
type MCPState struct {
Credential MCPCredential
// Probe is the last Test of this server, nil when nobody has asked.
Probe *MCPProbe
}
MCPState is everything the token-holder knows about one declared server. internal/web cannot import internal/mcp (depguard), and should not: a saved login is not this package's business, and the interface is what keeps it that way.
type Manager ¶
type Manager interface {
// Set applies a configuration under name, creating the pipeline if this
// daemon does not hold one. req.ExpectSHA is the revision the sender
// diffed against: empty means "I did not look", and a mismatch is refused
// rather than applied over whatever arrived in between.
Set(ctx context.Context, name string, req SetRequest) (SetResult, error)
Destroy(ctx context.Context, name string) error
Rename(ctx context.Context, from, to string) error
}
Manager applies what a `steps pipeline` verb asks for. An interface for the reason Runner is one: this package serves the surface and chooses neither a store driver nor a workspace provider.
type Option ¶
type Option func(*Server)
Option configures a server at construction, which is where the middleware table is built: an auth setting applied afterwards would be a server that answered without it first.
func WithActivityLog ¶
WithActivityLog says how to open the database the log lives in, for when every pipeline is gone and the log still holds what was done to them, and how many entries it keeps (zero is the default).
func WithAllowedHosts ¶
WithAllowedHosts refuses every request whose Host is not one of these names or a loopback one. The operator's to give, because a reverse proxy forwards the client's Host, so only the operator knows which names are this daemon's; WithLoopbackHosts is the list implied when nobody gave one.
func WithBasicAuth ¶
WithBasicAuth adds one account given in the clear. Both halves are the caller's to validate — see cli's WebCmd, where half a pair refuses to start.
func WithCapacity ¶
WithCapacity is where the follow page reads --max-runs from. A separate option rather than part of Runner because a read-only server has no Runner and still drains under the same limit.
func WithExporter ¶
WithExporter lets the server serve exports, built by exporter.
func WithExternalURL ¶
WithExternalURL is the address people and agents reach the daemon on, which every OAuth URL it publishes is built from.
func WithLocalUsers ¶
WithLocalUsers adds hashed accounts; a name given twice is the caller's to refuse.
func WithLoopbackHosts ¶ added in v0.2.0
WithLoopbackHosts answers loopback names and these, for a daemon bound to loopback that was given no WithAllowedHosts: a browser can reach it by no other name, so one arriving under another is a rebinding page. The names are the ones it was told it is published at, which a proxy on this machine forwards. A list given with WithAllowedHosts replaces this one, in either order.
func WithSessions ¶
WithSessions lets browsers sign in through a form, keeping their sessions in accounts.
func WithSignInProviders ¶
func WithSignInProviders(providers ...SignInProvider) Option
WithSignInProviders adds the providers the sign-in page offers.
func WithTracing ¶ added in v0.2.0
func WithTracing(m echo.MiddlewareFunc) Option
WithTracing installs m ahead of every other middleware; it is how internal/cli traces requests without this package importing a telemetry SDK.
func WithVersion ¶
WithVersion is the build version the footer prints; "dev" when unset, the same default steps --version has.
type Pipeline ¶
type Pipeline struct {
*scheduler.Pipeline
// Hooks answers POST /p/<slug>/hooks/<resource>, a delivery to a webhook resource. Built by the caller (trigger.HookHandler): this package serves the surface and does not own the queue — internal/scheduler drains it, reached only through Runner. Nil only in a test.
Hooks func(w http.ResponseWriter, r *http.Request, resource string)
// contains filtered or unexported fields
}
Pipeline is one held pipeline as this server sees it: the scheduler's handle (name, store, bus, served configuration) plus what only the HTTP surface needs.
func NewPipeline ¶
NewPipeline builds a served pipeline around the configuration it starts with. A literal would leave the embedded handle nil.
type PipelineConfig ¶
type PipelineConfig struct {
Name string `json:"name"`
SHA string `json:"sha"`
Source string `json:"source"`
Includes map[string][]byte `json:"includes,omitempty"`
From string `json:"from,omitempty"`
Paused bool `json:"paused"`
}
PipelineConfig is what get prints and what set diffs against: the source, its includes (bytes, for the reason SetRequest's are), and the sha that names both.
type PipelineSummary ¶
type PipelineSummary struct {
Name string `json:"name"`
SHA string `json:"sha"`
From string `json:"from,omitempty"`
Jobs int `json:"jobs"`
Paused bool `json:"paused"`
// Broken is why a pipeline the daemon holds is not being served — a restart could not restore it — and empty for one that is.
Broken string `json:"broken,omitempty"`
}
PipelineSummary is one row of `steps pipeline list`. Exported and shared with the client for the reason SetRequest is: two copies of a wire struct drift into a silently-dropped field rather than a build error.
type PlacementView ¶
PlacementView is one placed step's machine as the template reads it.
Exported because `steps runs where` renders the same rows through it. Two spellings of "which machine" had already drifted: the CLI's copy never learned Volatile, so the terminal — where an operator debugging a placed step looks first — reported a tmpfs workdir as an ordinary disk.
func (PlacementView) Disk ¶ added in v0.2.0
func (p PlacementView) Disk() string
Disk is what the step's own volumes held on the worker, "-" where nothing weighed them: a worker that keeps no volumes is not one whose step wrote nothing.
func (PlacementView) Identity ¶
func (p PlacementView) Identity() string
Identity is who the step ran as, blank when the worker did not say — never an invented 0, which would read as root.
func (PlacementView) Machine ¶
func (p PlacementView) Machine() string
Machine names the host, and the image if the step ran in a container on it.
func (PlacementView) Platform ¶
func (p PlacementView) Platform() string
Platform is what the worker reported itself to be.
func (PlacementView) Received ¶
func (p PlacementView) Received() string
Received is what came back from it: the tree the step produced there. A worker keeps what it produces too, so this is the cost of a LOCAL reader wanting the bytes, not of the step having made them.
func (PlacementView) Sent ¶
func (p PlacementView) Sent() string
Sent is what actually crossed to reach this machine. A worker keeps what it receives, so a step whose inputs were already there honestly reads 0 B.
type Roles ¶
type Roles struct {
// contains filtered or unexported fields
}
Roles are compiled rules; the zero value grants everything to anyone signed in, which is a daemon that has not been told otherwise.
func CompileRoles ¶
CompileRoles reads "role=expression" rules. Compiled here, once, so a rule that cannot work stops the daemon before it serves anyone. Rules are expected about who is allowed, so an admin rule is required: without one nobody could set a pipeline, and the daemon would be stuck as it stands.
func (Roles) AllowsProviders ¶
AllowsProviders refuses rules too loose for a daemon that signs people in with GitHub or Google. Those let in anybody with an account there, so the default every other daemon gets — everyone signed in may do everything — would make the whole internet an admin, and a missing viewer rule would let all of it read. Checked where the CLI starts and again where the server is built, so neither path can serve a provider without saying who of its accounts may do what.
type Runner ¶
type Runner interface {
// Enqueue queues a person's trigger of a job, returning the queue row id.
Enqueue(ctx context.Context, pipeline *scheduler.Pipeline, jobName, reason string, force bool) (int64, error)
// EnqueueRerun queues a retry of a recorded run.
EnqueueRerun(ctx context.Context, pipeline *scheduler.Pipeline, jobName, runID string) error
// Abort cancels a run this process is executing, and reports false when it is not running here.
Abort(pipeline *scheduler.Pipeline, runID string) bool
// AbortQueued drops a job's queued run before it starts, and reports false when nothing was queued.
AbortQueued(ctx context.Context, pipeline *scheduler.Pipeline, jobName string) (bool, error)
}
Runner is what the web layer needs in order to act rather than only report: enqueue a job for execution, and report what is currently running. Implemented by scheduler.LocalRunner, which internal/cli installs; an interface so the HTTP layer can be tested without starting real jobs.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server serves whatever has been set into it. The registry is behind a lock rather than fixed at construction because a handler, the drain and the poll loop all read it while another request writes it.
func New ¶
New builds a server over whatever pipelines it is handed, which may be none: a daemon is configured by `steps pipeline set` and by nothing else, so empty is the ordinary starting state rather than an error.
func (*Server) Broken ¶
func (s *Server) Broken() []BrokenPipeline
Broken is every pipeline held and not served, by name.
func (*Server) BrokenReason ¶
BrokenReason is why a pipeline is held and not served, and false when it is not broken.
func (*Server) ClearBroken ¶
ClearBroken forgets a broken pipeline, once nothing holds it any more.
func (*Server) Close ¶
Close lets go of every export's upstream connections. Pages need nothing closed; an export holds a live MCP connection per server it reaches.
func (*Server) Lookup ¶
Lookup is the pipeline served under slug, nil when this daemon holds none.
One return rather than the comma-ok a map gives, because the two say the same thing and a caller that reads the bool and keeps the pointer is exactly the shape a nil-flow analyzer cannot follow.
func (*Server) MarkBroken ¶
func (s *Server) MarkBroken(broken BrokenPipeline)
MarkBroken lists a pipeline as held and not served.
func (*Server) Remove ¶
Remove hands the pipeline back so the caller can shut down what it started for it, and nil when this daemon was not serving one.
func (*Server) SetManager ¶
SetManager is separate from New because the manager needs the server it registers into.
type SetRequest ¶
type SetRequest struct {
Source string `json:"source"`
// A daemon has no sibling filesystem, so an include that does not travel here cannot be resolved at all (see config.Bundle); bytes rather than strings because encoding/json replaces each invalid UTF-8 byte of a string with U+FFFD, which ran a Latin-1 run_file: as bytes nobody sent.
Includes map[string][]byte `json:"includes,omitempty"`
// ExpectSHA is the compare-and-set. Empty means the sender did not look, which a script may legitimately do.
ExpectSHA string `json:"expect_sha,omitempty"`
// From is the SENDER's path, recorded for a reader wondering where a served configuration came from, and never opened here.
From string `json:"from,omitempty"`
// Pause is written with the revision, before the pipeline is started: a pause sent after the set is a second request, and the first poll — and whatever it triggers — can land between the two.
Pause bool `json:"pause,omitempty"`
}
SetRequest is one upload: the substituted YAML, the files it includes, and the revision the sender believed it was replacing.
type SetResult ¶
type SetResult struct {
SHA string `json:"sha"`
// Created, replaced and unchanged are three outcomes a person reads differently, so the answer says which.
Created bool `json:"created"`
Unchanged bool `json:"unchanged"`
// Repaired is a set that served a pipeline a restart had left broken.
Repaired bool `json:"repaired,omitempty"`
}
SetResult is what the daemon did, so the terminal that asked can say so.
type SignInProvider ¶
type SignInProvider struct {
// Name is the provider's word in URLs and in an actor's kind ("github").
Name string
// Label is what the sign-in button says it continues with.
Label string
// contains filtered or unexported fields
}
SignInProvider is one way to sign in that the daemon does not keep the password for.
func GitHubSignIn ¶
func GitHubSignIn(clientID, secret, externalURL string, client *http.Client) SignInProvider
GitHubSignIn signs people in with GitHub, reading their organizations and teams so rules can match "acme/eng". client is nil outside a test that answers for github.com.
func GoogleSignIn ¶
func GoogleSignIn(clientID, secret, externalURL string, client *http.Client) SignInProvider
GoogleSignIn signs people in with Google. Google says nothing about groups without its admin API, so rules for these people match on email or domain.
Source Files
¶
- abort.go
- actionbar.go
- activity.go
- api.go
- approvals.go
- attention.go
- auth.go
- config.go
- connections.go
- controls.go
- detect.go
- dials.go
- docs.go
- exportpage.go
- exports.go
- follow.go
- graph.go
- hosts.go
- jobs.go
- jsonview.go
- live.go
- mark.go
- mcp.go
- mcplogin.go
- middleware.go
- model.go
- nav.go
- oauthserver.go
- overview.go
- prose.go
- providers.go
- reads.go
- registry.go
- render.go
- resources.go
- roles.go
- run.go
- search.go
- sessions.go
- ttlmap.go
- versionstatus.go
- web.go