Documentation
¶
Overview ¶
Package runner implements `sprout runner`: a user's own machine executing the browser build's escalated work for their platform account (SP-159). The platform side of the protocol is documented in the platform repo's docs/runners/PROTOCOL.md.
Index ¶
- Constants
- Variables
- func DefaultImage() string
- func DeleteAPIKey() error
- func Dir() (string, error)
- func DockerAvailable(ctx context.Context, docker string) error
- func InstallService() (string, error)
- func IsGatewayProvider(provider string) bool
- func LoadAPIKey() (string, error)
- func LocalDir(raw string) (string, error)
- func OpenURL(raw string)
- func SaveAPIKey(key string) (source string, err error)
- func UninstallService() error
- func ValidMode(m string) bool
- func ValidateImage(image string) error
- func WorkspacesDir() (string, error)
- type Client
- func (c *Client) PollLink(ctx context.Context, deviceCode string) (*Credentials, error)
- func (c *Client) PollWorkspaceTasks(ctx context.Context) ([]WorkspaceTask, error)
- func (c *Client) SendHeartbeat(ctx context.Context, hb Heartbeat) error
- func (c *Client) StartLink(ctx context.Context, name, goos, arch, mode string) (*DeviceStart, error)
- func (c *Client) SubmitWorkspaceResult(ctx context.Context, r WorkspaceResult) error
- func (c *Client) UpdateWorkspaceStatus(ctx context.Context, workspaceID, status string) error
- type ContainerLauncher
- func (l *ContainerLauncher) Destroy(ctx context.Context, workspaceID string, _ *Workspace) error
- func (l *ContainerLauncher) EnsureImage(ctx context.Context, progress io.Writer) error
- func (l *ContainerLauncher) Start(ctx context.Context, task WorkspaceTask) (*Workspace, error)
- func (l *ContainerLauncher) Stop(ctx context.Context, ws *Workspace) error
- type Credentials
- type DeviceStart
- type Heartbeat
- type HostLauncher
- type HostServer
- type ImageUnavailableError
- type Launcher
- type Runner
- type State
- type Workspace
- type WorkspaceResult
- type WorkspaceTask
Constants ¶
const ( ModeContainer = "container" ModeNative = "native" ModeBareMetal = "bare-metal" )
Modes a runner executes workspaces in (SP-159 §159c).
const ( // CapabilityLocalDirs: the runner serves workspace_dir tasks for the // directories in local_dirs. CapabilityLocalDirs = "local_dirs" // CapabilityGateway: the runner wires llm_provider "gateway" tasks to // the platform gateway (gateway_url) with the workspace key. CapabilityGateway = "gateway" // CapabilityTaskErrors: failed workspace results carry an error reason. CapabilityTaskErrors = "task_errors" )
Optional start-task features a runner advertises in its heartbeat.
const DefaultListenAddr = "127.0.0.1:8935"
DefaultListenAddr is where the host server listens for platform calls.
const GatewayKeyEnvVar = "SPROUT_GATEWAY_KEY"
GatewayKeyEnvVar carries the gateway key in the workspace daemon's environment. The key's scope, lifetime and revocation are the platform's: the runner only passes it to the daemon, never writes it to disk and never sends it anywhere but the gateway endpoint. Code running in the workspace can read the daemon's environment, so the platform should mint a key scoped to the one workspace and revoke it when the workspace ends.
const GatewayProviderName = "gateway"
GatewayProviderName is the workspace provider name for the platform gateway's OpenAI-compatible endpoint. A start task naming this provider wires the workspace's agent to the gateway with the task's gateway key, so the workspace uses the gateway's models (and the user's gateway quotas) instead of a public provider's endpoint.
const WorkspaceImageRepo = "ghcr.io/sprout-foundry/sprout-workspace"
WorkspaceImageRepo is the public workspace image, built from docker/workspace and published for every release.
Variables ¶
var ( ErrAuthorizationPending = errors.New("authorization pending") ErrSlowDown = errors.New("slow down") ErrAccessDenied = errors.New("link request denied on the platform") ErrExpiredToken = errors.New("link code expired") )
Device-flow poll outcomes the caller acts on.
Functions ¶
func DefaultImage ¶
func DefaultImage() string
DefaultImage is the workspace image container mode runs unless the owner chose another with --image: the image built for this runner's own release, so the daemon in the workspace matches the runner, or :latest for source and pre-release builds.
func DeleteAPIKey ¶
func DeleteAPIKey() error
DeleteAPIKey removes the runner's API key from both stores.
func DockerAvailable ¶
DockerAvailable reports whether container mode can run here.
func InstallService ¶
InstallService registers `sprout runner start` as a systemd user unit that starts at login and restarts if it exits.
func IsGatewayProvider ¶ added in v0.26.0
IsGatewayProvider reports whether a start task's llm_provider names the platform gateway.
func LoadAPIKey ¶
LoadAPIKey reads the runner's API key from the credential store, or the file fallback SaveAPIKey may have used.
func LocalDir ¶ added in v0.26.0
LocalDir validates and normalizes a user-named directory the runner may serve workspaces in place: it must exist, be absolute, contain no ':' (the docker volume syntax would misparse it), and be a directory. The returned path is symlink-resolved — the same form start tasks are compared in — so an allowlist entry and a task naming the same directory through a symlink (macOS /tmp, a homedir symlink) match. A tilde leading the path expands to the user's home (flag ergonomics; the stored state and the protocol carry the expanded absolute path).
func OpenURL ¶
func OpenURL(raw string)
OpenURL opens an http(s) URL in the user's browser, best effort: the link flow prints the URL too, so a failure here costs nothing. The URL comes from the platform's response, so anything else (file:, app launchers, a value that would parse as a flag) is not opened.
func SaveAPIKey ¶
SaveAPIKey stores the runner's API key in the credential store. The platform issues the key exactly once, so when the OS keyring refuses (no GUI session, locked keychain) it falls back to sprout's encrypted file store rather than lose it.
func UninstallService ¶
func UninstallService() error
UninstallService disables and removes the systemd user unit.
func ValidateImage ¶ added in v0.27.0
ValidateImage rejects references that are empty, contain whitespace or start with "-": the reference is passed to the docker CLI as an argument and must never be read as a flag.
func WorkspacesDir ¶
WorkspacesDir holds one clone per workspace; a runner never touches the user's own working copies. It lives in sprout's state dir, outside the config dir whose credential files native mode hides from workspaces.
Types ¶
type Client ¶
type Client struct {
BaseURL string
Creds Credentials
HTTPClient *http.Client
}
Client speaks the runner protocol to one platform.
func NewClient ¶
func NewClient(baseURL string, creds Credentials) *Client
NewClient returns a client for baseURL; creds may be empty before linking.
func (*Client) PollLink ¶
PollLink checks whether the user approved the link. It returns the runner's credentials exactly once, on approval.
func (*Client) PollWorkspaceTasks ¶
func (c *Client) PollWorkspaceTasks(ctx context.Context) ([]WorkspaceTask, error)
PollWorkspaceTasks fetches pending workspace instructions.
func (*Client) SendHeartbeat ¶
SendHeartbeat reports liveness and capability.
func (*Client) StartLink ¶
func (c *Client) StartLink(ctx context.Context, name, goos, arch, mode string) (*DeviceStart, error)
StartLink asks the platform for a device code for this machine. mode is shown on the approval screen; the platform never stores or sets it.
func (*Client) SubmitWorkspaceResult ¶
func (c *Client) SubmitWorkspaceResult(ctx context.Context, r WorkspaceResult) error
SubmitWorkspaceResult reports a workspace start outcome.
type ContainerLauncher ¶
type ContainerLauncher struct {
Image string
// Docker is the CLI to run; "docker" when empty.
Docker string
}
ContainerLauncher runs each workspace in the sprout workspace image with the platform's hardening profile: non-root, read-only rootfs, every capability dropped, no-new-privileges, CPU and memory limits, the daemon published on loopback only. It drives the docker CLI, so Docker Desktop, OrbStack, Colima and Podman's docker shim all work.
func (*ContainerLauncher) EnsureImage ¶ added in v0.27.0
EnsureImage makes sure the workspace image is present, pulling it when it is not, so a missing image or registry credential fails before the runner goes online rather than at its first task.
func (*ContainerLauncher) Start ¶
func (l *ContainerLauncher) Start(ctx context.Context, task WorkspaceTask) (*Workspace, error)
type Credentials ¶
Credentials identify a linked runner.
type DeviceStart ¶
type DeviceStart struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
Interval int `json:"interval"`
ExpiresIn int `json:"expires_in"`
}
DeviceStart is the platform's answer to a link request.
type Heartbeat ¶
type Heartbeat struct {
Status string `json:"status"`
RunningTasks int `json:"running_tasks"`
Mode string `json:"mode"`
Sandbox string `json:"sandbox"`
RunnerVersion string `json:"runner_version"`
Toolchains []string `json:"toolchains,omitempty"`
// LocalDirs are the user-named directories (see `sprout runner start
// --dir`) this runner serves workspaces in place. A start task naming
// one of them runs against the user's real files, no clone.
LocalDirs []string `json:"local_dirs,omitempty"`
// Capabilities lists the optional start-task features this runner
// serves (Capability* constants). The platform sends workspace_dir or
// a gateway provider only to runners that advertise them: an older
// runner ignores workspace_dir and would hand a gateway key to a
// public provider.
Capabilities []string `json:"capabilities,omitempty"`
// DirectURL is where the platform reaches the host server now; empty
// for a relayed runner.
DirectURL string `json:"direct_url,omitempty"`
// Relayed reports that the runner serves the platform over its tunnel.
Relayed bool `json:"relayed,omitempty"`
}
Heartbeat reports liveness plus what the runner can do. The platform only reflects Mode; it never sets it.
type HostLauncher ¶
type HostLauncher struct {
Sandboxed bool
// Writable are extra paths native mode may write (toolchain caches).
Writable []string
// SproutBin is the binary that serves the workspace daemon.
SproutBin string
// contains filtered or unexported fields
}
HostLauncher runs a workspace's daemon directly on this machine, with its toolchains. Sandboxed is native mode (the OS sandbox confines it); unsandboxed is bare-metal mode (it runs with the user's full access).
func (*HostLauncher) Start ¶
func (l *HostLauncher) Start(ctx context.Context, task WorkspaceTask) (*Workspace, error)
type HostServer ¶
type HostServer struct {
// contains filtered or unexported fields
}
HostServer is the only surface the platform reaches on a runner: /daemon/{workspaceID}/... proxied to that workspace's daemon on loopback, after checking the per-workspace bearer secret. The daemon checks the same secret again (its SPROUT_AUTH_TOKEN), so the runner is not the only gate.
func (*HostServer) Bind ¶
func (h *HostServer) Bind(workspaceID string, port int, secret string)
Bind routes a workspace to its daemon port, authenticated by secret.
func (*HostServer) Handler ¶
func (h *HostServer) Handler() http.Handler
Handler is the host server's HTTP surface.
func (*HostServer) Unbind ¶
func (h *HostServer) Unbind(workspaceID string)
Unbind removes a workspace; later calls for it get 404.
type ImageUnavailableError ¶ added in v0.27.0
type ImageUnavailableError struct {
}
ImageUnavailableError reports that the workspace image is neither present locally nor pullable. The runner never holds registry credentials: the docker CLI pulls with Docker's own credential store, so the fix is `docker login` or a different image or mode.
func (*ImageUnavailableError) Error ¶ added in v0.27.0
func (e *ImageUnavailableError) Error() string
type Launcher ¶
type Launcher interface {
Start(ctx context.Context, task WorkspaceTask) (*Workspace, error)
Stop(ctx context.Context, ws *Workspace) error
Destroy(ctx context.Context, workspaceID string, ws *Workspace) error
}
Launcher starts and stops workspaces in one execution mode.
type Runner ¶
type Runner struct {
State *State
Client *Client
Launcher Launcher
Host *HostServer
// Sandbox and Version are reported in heartbeats.
Sandbox string
Version string
Log *slog.Logger
// contains filtered or unexported fields
}
Runner serves workspaces for one platform account until its context ends.
type State ¶
type State struct {
PlatformURL string `json:"platform_url"`
RunnerID string `json:"runner_id"`
Name string `json:"name"`
Mode string `json:"mode"`
// PublicURL is the HTTPS base the platform reaches the host server at
// (direct mode). Empty means the relay tunnel, once the platform offers it.
PublicURL string `json:"public_url,omitempty"`
ListenAddr string `json:"listen_addr,omitempty"`
Image string `json:"image,omitempty"`
// Writable are extra paths native mode may write (toolchain caches).
Writable []string `json:"writable,omitempty"`
// LocalDirs are the directories the user named (with `sprout runner
// start --workspace`, repeatable) that this runner serves workspaces
// in place: a start task naming one runs against the user's real
// files there, with no clone. Explicitly opt-in; never platform-set.
LocalDirs []string `json:"local_dirs,omitempty"`
}
State is the runner's persistent configuration. The API key is not part of it: it lives in the credential store.
type Workspace ¶
type Workspace struct {
ID string
Port int
// Handle identifies it to its launcher (a container name, or a pid).
Handle string
}
Workspace is a running workspace's daemon, reachable on loopback.
type WorkspaceResult ¶
type WorkspaceResult struct {
WorkspaceID string `json:"workspace_id"`
ContainerID string `json:"container_id"`
Port int `json:"port"`
ConnectionURL string `json:"connection_url,omitempty"`
Status string `json:"status"`
// Error says why a "failed" start was refused or failed, in words safe
// to show the user (never a secret or a raw launcher error).
Error string `json:"error,omitempty"`
}
WorkspaceResult reports a started (or failed) workspace.
type WorkspaceTask ¶
type WorkspaceTask struct {
WorkspaceID string `json:"workspace_id"`
UserID string `json:"user_id"`
RepoURL string `json:"repo_url"`
// WorkspaceDir, when set, names an allowlisted local directory the
// workspace runs in place: no clone, the directory is the workspace
// root and the runner writes the user's real files there. The runner
// refuses any path it was not explicitly configured to serve.
WorkspaceDir string `json:"workspace_dir,omitempty"`
Action string `json:"action"`
ContainerID string `json:"container_id"`
TxnSecret string `json:"txn_secret,omitempty"`
GitToken string `json:"git_token,omitempty"`
LLMProvider string `json:"llm_provider,omitempty"`
LLMKey string `json:"llm_key,omitempty"`
// Model names the model the workspace starts on (SPROUT_MODEL), for
// any provider. The platform decides what to send, e.g. the user's
// last-used model; empty leaves the daemon's normal default for the
// provider.
Model string `json:"model,omitempty"`
UserEnv map[string]string `json:"user_env,omitempty"`
PlatformAPIURL string `json:"platform_api_url,omitempty"`
// GatewayURL is the gateway's OpenAI-compatible base (the URL that
// /chat/completions and /models hang off) for a gateway task. Empty
// derives it from PlatformAPIURL plus the platform's gateway path.
GatewayURL string `json:"gateway_url,omitempty"`
}
WorkspaceTask is a start/stop/destroy instruction for one workspace. Secrets arrive over the runner-key-authenticated channel and are never written to disk by the runner.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package relay carries HTTP exchanges over one WebSocket between the platform and a runner behind NAT (platform SP-BUILDER-13, sprout SP-159).
|
Package relay carries HTTP exchanges over one WebSocket between the platform and a runner behind NAT (platform SP-BUILDER-13, sprout SP-159). |
|
Package sandbox confines a runner workspace's processes in native mode (SP-159 §159c): host toolchains under the OS sandbox — Seatbelt on macOS, bubblewrap on Linux.
|
Package sandbox confines a runner workspace's processes in native mode (SP-159 §159c): host toolchains under the OS sandbox — Seatbelt on macOS, bubblewrap on Linux. |