runner

package
v0.29.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

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

View Source
const (
	ModeContainer = "container"
	ModeNative    = "native"
	ModeBareMetal = "bare-metal"
)

Modes a runner executes workspaces in (SP-159 §159c).

View Source
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.

View Source
const DefaultListenAddr = "127.0.0.1:8935"

DefaultListenAddr is where the host server listens for platform calls.

View Source
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.

View Source
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.

View Source
const WorkspaceImageRepo = "ghcr.io/sprout-foundry/sprout-workspace"

WorkspaceImageRepo is the public workspace image, built from docker/workspace and published for every release.

Variables

View Source
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 Dir

func Dir() (string, error)

Dir is where the runner keeps its state and workspaces.

func DockerAvailable

func DockerAvailable(ctx context.Context, docker string) error

DockerAvailable reports whether container mode can run here.

func InstallService

func InstallService() (string, error)

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

func IsGatewayProvider(provider string) bool

IsGatewayProvider reports whether a start task's llm_provider names the platform gateway.

func LoadAPIKey

func LoadAPIKey() (string, error)

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

func LocalDir(raw string) (string, error)

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

func SaveAPIKey(key string) (source string, err error)

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 ValidMode

func ValidMode(m string) bool

ValidMode reports whether m is a known execution mode.

func ValidateImage added in v0.27.0

func ValidateImage(image string) error

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

func WorkspacesDir() (string, error)

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 (c *Client) PollLink(ctx context.Context, deviceCode string) (*Credentials, error)

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

func (c *Client) SendHeartbeat(ctx context.Context, hb Heartbeat) error

SendHeartbeat reports liveness and capability.

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.

func (*Client) UpdateWorkspaceStatus

func (c *Client) UpdateWorkspaceStatus(ctx context.Context, workspaceID, status string) error

UpdateWorkspaceStatus reports a workspace's status change.

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) Destroy

func (l *ContainerLauncher) Destroy(ctx context.Context, workspaceID string, _ *Workspace) error

func (*ContainerLauncher) EnsureImage added in v0.27.0

func (l *ContainerLauncher) EnsureImage(ctx context.Context, progress io.Writer) error

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 (*ContainerLauncher) Stop

func (l *ContainerLauncher) Stop(ctx context.Context, ws *Workspace) error

type Credentials

type Credentials struct {
	RunnerID string `json:"runner_id"`
	APIKey   string `json:"api_key"`
}

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) Destroy

func (l *HostLauncher) Destroy(_ context.Context, workspaceID string, _ *Workspace) error

func (*HostLauncher) Start

func (l *HostLauncher) Start(ctx context.Context, task WorkspaceTask) (*Workspace, error)

func (*HostLauncher) Stop

func (l *HostLauncher) Stop(_ context.Context, ws *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 NewHostServer

func NewHostServer() *HostServer

NewHostServer returns an empty host server.

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 {
	Image  string
	Output string
}

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.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context) error

Run serves the host server, heartbeats and executes workspace tasks.

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.

func LoadState

func LoadState() (*State, error)

LoadState returns the saved state, or defaults when the runner was never linked.

func (*State) Linked

func (s *State) Linked() bool

Linked reports whether the runner has completed `sprout runner link`.

func (*State) Save

func (s *State) Save() error

Save writes the state with owner-only permissions.

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.

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.

Jump to

Keyboard shortcuts

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