server

package
v0.0.36 Latest Latest
Warning

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

Go to latest
Published: Jun 24, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package server ties the Docker box manager to two front-ends that share one process:

  • an MCP server (streamable HTTP), used by a chatbot to create/list/destroy boxes. It only ever exchanges box IDs and the *auth page URL* — never the OAuth secret.
  • a small web server that serves the auth page where the user pastes their OAuth code. The code goes browser -> this server -> container stdin, so it never enters the chat/MCP context.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Authenticator

type Authenticator struct {
	// contains filtered or unexported fields
}

Authenticator gates box activation behind provider sign-in. A nil *Authenticator means activation is unauthenticated (no provider configured).

func NewAuthenticator

func NewAuthenticator(ctx context.Context, cfg config.AuthConfig) (*Authenticator, error)

NewAuthenticator builds an Authenticator from the auth configuration, doing OIDC discovery for each enabled provider. It returns (nil, nil) when no provider is enabled, which leaves activation unauthenticated.

@arg ctx Context for provider discovery. @arg cfg The auth configuration (providers, session TTL). @return *Authenticator The authenticator, or nil when no provider is enabled. @error error if an enabled provider cannot be discovered.

@testcase TestNewAuthenticatorDisabled returns nil when no provider is enabled.

func (*Authenticator) AdminEnabled

func (a *Authenticator) AdminEnabled() bool

AdminEnabled reports whether the admin UI is enabled (an admin allow-list is configured). It is false on a nil Authenticator.

@return bool True when at least one admin email is configured.

@testcase TestAdminAllowlist reports enabled only when emails are configured.

type LoginStore

type LoginStore interface {
	// SaveLoginFlow stores the in-flight handshake state under the OAuth state key.
	SaveLoginFlow(state string, f loginFlow) error
	// TakeLoginFlow returns and removes the flow for state (one-time use); the bool
	// is false when no flow matches.
	TakeLoginFlow(state string) (loginFlow, bool, error)
	// SaveLoginSession stores a completed login session under its opaque id.
	SaveLoginSession(id string, s loginSession) error
	// LoginSession returns the session for id; the bool is false when none matches.
	LoginSession(id string) (loginSession, bool, error)
	// DeleteLoginSession removes a login session; deleting a missing id is a no-op.
	DeleteLoginSession(id string) error
	// PurgeExpiredLogins drops login sessions and flows that expired before now.
	PurgeExpiredLogins(now time.Time) error
}

LoginStore persists the activation login state across restarts: completed login sessions (keyed by an opaque cookie ID) and the short-lived in-flight OIDC handshake state (keyed by the OAuth state parameter). All methods must be safe for concurrent use.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server orchestrates boxes and owns the session registry.

func New

func New(mgr boxManager, hooks boxHooks, publicURL string, authTTL time.Duration, store Store, auth *Authenticator) *Server

New builds a Server. publicURL is the externally reachable base URL used to construct auth page links; authTTL is how long a box may stay un-authenticated before the reaper destroys it. store persists the session registry; pass noopStore{} to disable persistence. hooks runs lifecycle hooks per box; pass nil to disable hook integration. auth gates box activation behind provider sign-in; pass nil to leave activation unauthenticated. Call Restore to load any saved sessions.

@arg mgr The box manager the server delegates Docker operations to. @arg hooks The box lifecycle hook runner; nil disables hook integration. @arg publicURL The externally reachable base URL for auth page links. @arg authTTL How long a box may stay un-authenticated before being reaped. @arg store The session store used to persist the registry; noopStore{} disables it. @arg auth The activation authenticator; nil leaves activation unauthenticated. @return *Server A ready-to-use Server with an empty in-memory session registry.

@testcase TestCreateBoxRegistersSession builds a Server via New. @testcase TestCreateBoxRunsCreateHooks builds a Server with a hook runner.

func (*Server) AuthPageURL

func (s *Server) AuthPageURL(tok string) string

AuthPageURL is the URL the user opens to finish authentication.

@arg tok The session token identifying the auth session. @return string The absolute auth page URL for the token.

@testcase TestCreateBoxRegistersSession checks the auth page URL format.

func (*Server) BoxExec

func (s *Server) BoxExec(ctx context.Context, boxID, command string) (docker.ExecResult, error)

BoxExec runs a shell command inside the box with the given box ID and returns its captured output. Like get, logs, and destroy, it is keyed by the box ID supplied at create time, so a box created without one is not reachable here. The command is run via "/bin/sh -c" so callers can pass an ordinary shell line.

@arg ctx Context for the exec request. @arg boxID The box ID of the box to run the command in. @arg command The shell command line to run inside the box. @return docker.ExecResult The command's stdout, stderr, and exit code. @error error if the command is empty, no box has that box ID, its spoke is not connected, or the command cannot be run.

@testcase TestBoxExecByBoxID runs a command in a box looked up by box ID.

func (*Server) BoxLogs

func (s *Server) BoxLogs(ctx context.Context, boxID string, tail int) (string, error)

BoxLogs returns the recent console output of the box with the given box ID. Like get and destroy, it is keyed by the box ID supplied at create time, so a box created without one is not reachable here. tail bounds how many trailing lines are returned and is passed through to the manager.

@arg ctx Context for the logs request. @arg boxID The box ID of the box to read logs from. @arg tail The maximum number of trailing log lines to return; the manager applies a default when non-positive. @return string The box's recent console output. @error error if no box has that box ID, its spoke is not connected, or the logs cannot be read.

@testcase TestBoxLogsByBoxID returns a box's logs looked up by box ID.

func (*Server) CreateBox

func (s *Server) CreateBox(ctx context.Context, opts docker.CreateOptions) (*session, error)

CreateBox launches a new box and registers an auth session for it. When box hooks are configured, it first runs the box.create hooks, injects the files they return (e.g. a granular hook's subject token, config, and CLIs), and records their opaque state on the session; that state is replayed to the box.destroy hooks if box creation later fails so nothing is left dangling. It returns the session so callers can build the auth page URL. opts carries the optional image, box ID, description, and the spoke to place the box on (empty means the local in-process spoke).

@arg ctx Context for the box creation. @arg opts The optional image, box ID, description, and target spoke for the box. @return *session The registered auth session for the new box. @error error if the target spoke is not connected, a box.create hook fails, the box cannot be created, or a session token cannot be generated.

@testcase TestCreateBoxRegistersSession checks the session is registered with box ID/description. @testcase TestCreateBoxDestroysOnTokenFailure checks a create error propagates. @testcase TestCreateBoxRunsCreateHooks runs the hooks, injects their files, and persists their state. @testcase TestCreateBoxRunsDestroyHooksOnCreateFailure replays hook state when box creation fails. @testcase TestCreateBoxRoutesToSpoke creates the box on the named remote spoke. @testcase TestCreateBoxUnknownSpoke errors when the named spoke is not connected. @testcase TestCreateBoxDefaultsImageToBoxImage stamps the hub's box image when the request names none. @testcase TestCreateBoxKeepsExplicitImage leaves a request's explicit image untouched.

func (*Server) DestroyBox

func (s *Server) DestroyBox(ctx context.Context, idOrName string) error

DestroyBox destroys a box and forgets any session pointing at it.

@arg ctx Context for the destroy request. @arg idOrName The box ID or container ID identifying the box to destroy. @error error if the box's spoke is not connected, or the box cannot be destroyed.

@testcase TestDestroyForgetsSession checks the session is forgotten after destroy. @testcase TestDestroyRunsDestroyHooks checks the box's hook state is replayed to the destroy hooks. @testcase TestDestroyRoutesToSpoke destroys a box on the spoke its session names. @testcase TestDestroyBoxByBoxIDRoutesToSpoke destroys a remote box by its box ID.

func (*Server) Handler

func (s *Server) Handler(mcpServer *mcp.Server) http.Handler

Handler builds the HTTP handler serving the MCP endpoint (at the root), the auth web pages (at /auth/{token}), a /healthz probe, and the server favicon. When clustering is enabled (a hub was set), it also serves the spoke connection endpoint at /spoke/connect, and when an admin allow-list is configured it serves the admin UI at /admin. mcpServer is reused across sessions.

@arg mcpServer The MCP server shared across all requests to the root endpoint. @return http.Handler A mux routing the MCP, auth, health, and favicon endpoints.

@testcase TestAuthPageRendersAndSubmits drives the auth routes through this handler. @testcase TestHealthz checks the /healthz route returns ok. @testcase TestFaviconServed checks the favicon route returns the embedded SVG.

func (*Server) ListBoxes

func (s *Server) ListBoxes(ctx context.Context) ([]docker.Box, error)

ListBoxes returns all managed boxes across every spoke, each tagged with the spoke it runs on. The local spoke must list successfully; a connected remote spoke that errors is logged and skipped so one bad spoke doesn't fail the whole listing.

@arg ctx Context for the list request. @return []docker.Box The boxes managed by this server, tagged with their spoke. @error error if the local spoke cannot be listed.

@testcase TestMCPToolsRegisteredAndCreate exercises the server's box wiring. @testcase TestListFansOutAcrossSpokes aggregates and tags boxes from every spoke.

func (*Server) MCPServer

func (s *Server) MCPServer(name, version string) *mcp.Server

MCPServer builds an MCP server exposing the box tools. The OAuth secret is never an input or output of any tool: create returns only an auth page URL.

@arg name The MCP server implementation name. @arg version The MCP server implementation version. @return *mcp.Server An MCP server with the create/get/list/destroy tools registered.

@testcase TestMCPToolsRegisteredAndCreate checks all tools are registered and create works.

func (*Server) ReapLoop

func (s *Server) ReapLoop(ctx context.Context, every time.Duration, log func(string))

ReapLoop periodically destroys orphaned (never-authenticated) boxes and prunes their sessions. It blocks until ctx is cancelled.

@arg ctx Context whose cancellation stops the loop. @arg every How often to run a reap pass. @arg log Optional sink for reaper log messages; may be nil.

@testcase TestPruneSessionsAfterReap covers the session pruning ReapLoop relies on.

func (*Server) Restore

func (s *Server) Restore(ctx context.Context) (int, error)

Restore loads persisted sessions into the registry and reconciles them with the spokes: a session whose box no longer exists on its (reachable) spoke is dropped (and deleted from the store) so a stale token can't linger. A session whose spoke is not currently connected is kept — the box may still be alive, we just can't verify it yet. It returns the number of sessions restored. Call it once at startup, before serving.

@arg ctx Context for the spoke listings used to reconcile. @return int The number of sessions restored into the registry. @error error if the store cannot be read or the local spoke cannot be listed.

@testcase TestRestoreLoadsAndReconciles restores live sessions and drops dead ones. @testcase TestRestoreKeepsDisconnectedSpokeSessions keeps a session whose spoke is offline.

func (*Server) SetBoxImage added in v0.0.31

func (s *Server) SetBoxImage(image string)

SetBoxImage sets the hub's resolved per-box image (claude_image, or the built-in default when unset) that the server stamps onto a creation request naming none. Resolving the image on the hub keeps remote spokes config-free and defaultless: the spoke launches exactly the image it is sent.

@arg image The resolved per-box container image (e.g. ghcr.io/clems4ever/granular-llmbox-box:latest); never empty in practice.

@testcase TestCreateBoxDefaultsImageToBoxImage stamps the configured image onto an imageless request.

func (*Server) SetHub

func (s *Server) SetHub(hub clusterHub)

SetHub attaches the cluster hub holding connected remote spokes. Call it once at startup, before serving, when clustering is enabled. Without a hub the server runs single-host: every box uses the in-process "local" spoke.

@arg hub The cluster hub resolving remote spokes by name.

@testcase TestCreateBoxRoutesToSpoke routes a box to a remote spoke via the hub.

func (*Server) SetSpokeImage added in v0.0.29

func (s *Server) SetSpokeImage(image string)

SetSpokeImage sets the llmbox image named in the admin UI's ready-to-run spoke command. It is display-only and does not affect how spokes run.

@arg image The container image (e.g. ghcr.io/clems4ever/granular-llmbox:0.0.6).

@testcase TestAdminCreateSpokeMintsToken shows the configured image in the command.

func (*Server) SpokeStatuses

func (s *Server) SpokeStatuses(_ context.Context) ([]SpokeStatus, error)

SpokeStatuses reports every spoke and its health: the in-process "local" spoke plus each enrolled remote spoke, marking which are currently connected. With clustering disabled it returns just the local spoke.

@arg _ Context (unused; the data is in-memory and in the store). @return []SpokeStatus The local spoke followed by each enrolled remote spoke. @error error if the enrolled spokes cannot be read from the store.

@testcase TestSpokeStatusesReportsHealth marks enrolled spokes connected or not. @testcase TestSpokeStatusesLocalOnly returns just the local spoke without a hub.

func (*Server) SubmitCode

func (s *Server) SubmitCode(ctx context.Context, tok, code string) error

SubmitCode feeds the user's OAuth code to the box's login process and waits for the box to become ready. It is called by the web handler, never by MCP.

@arg ctx Context for the code submission. @arg tok The session token identifying the box. @arg code The OAuth code pasted by the user. @error error if the session is unknown, the code is empty, its spoke is not connected, or login fails.

@testcase TestSubmitCodeSuccess marks the session ready on success. @testcase TestSubmitCodeFailureRecorded records the error on failure. @testcase TestSubmitCodeUnknownToken errors for an unknown token. @testcase TestSubmitCodeEmpty errors for an empty code.

type SpokeStatus

type SpokeStatus struct {
	Name       string    `json:"name" jsonschema:"the spoke's name; 'local' is the in-process spoke"`
	Connected  bool      `json:"connected" jsonschema:"whether the spoke currently has a live connection to the hub"`
	Local      bool      `json:"local,omitempty" jsonschema:"true for the in-process spoke (always connected)"`
	EnrolledAt time.Time `json:"enrolled_at,omitempty" jsonschema:"when the remote spoke enrolled (absent for the local spoke)"`
}

SpokeStatus describes one cluster spoke and its health. The in-process spoke is always present and connected; each enrolled remote spoke is reported with whether it currently holds a live connection to the hub.

type Store

type Store interface {
	// Save writes (creating or replacing) one session keyed by its token.
	Save(ps persistedSession) error
	// Delete removes the session for a token; deleting a missing token is a no-op.
	Delete(token string) error
	// LoadAll returns every persisted session.
	LoadAll() ([]persistedSession, error)
	// Close releases the underlying store.
	Close() error

	LoginStore
	cluster.Store
}

Store persists the auth-session registry across restarts. All methods must be safe for concurrent use. Use OpenStore for a bbolt-backed implementation, or noopStore{} to disable persistence.

func OpenStore

func OpenStore(path string) (Store, error)

OpenStore opens (creating if needed) a bbolt-backed Store at path.

@arg path The filesystem path to the bbolt database file. @return Store A ready-to-use, bbolt-backed session store. @error error if the database cannot be opened or initialized.

@testcase TestBoltStoreRoundTrip opens a store and round-trips a session.

Jump to

Keyboard shortcuts

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