docker

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: 23 Imported by: 0

Documentation

Overview

Package docker wraps the Docker Engine API to manage the lifecycle of "llmboxes": containers that run Claude Code in remote-control mode, each authenticated by an end user via OAuth.

Lifecycle of a box:

  1. Create starts a container whose entrypoint runs `claude auth login`. The container has a TTY; the login process parks at a "paste code" prompt after printing an OAuth authorize URL. Create captures that URL and returns it. The box is named "llmbox-pending-<id>".
  2. SubmitCode writes the OAuth code (obtained out-of-band by the user) to the login process's stdin. On success the CLI stores credentials inside the container and the entrypoint execs `claude remote-control`, which prints a session URL. The box is renamed "llmbox-<id>" to mark it authenticated.
  3. ReapOrphans destroys boxes that are still "pending" past a TTL — e.g. a user who never finished authenticating, or boxes orphaned by a restart.

The OAuth code never passes through the MCP layer: it travels from the user's browser to this binary's web server to the container's stdin only.

Safety: every container created here carries ManagedLabel; list/destroy/reap operations are scoped to that label so unrelated host containers are untouched.

Index

Constants

View Source
const (
	// ManagedLabel marks every container created by this server.
	ManagedLabel = "com.llmbox.managed"

	// BoxIDLabel and DescriptionLabel persist the caller-assigned box ID and
	// description so List can report them straight from a container list
	// (ContainerList summaries carry labels but neither the box ID nor the
	// rest of the container config). The box ID is also set as the container
	// hostname, but the label is the authoritative copy List reads.
	BoxIDLabel       = "com.llmbox.box-id"
	DescriptionLabel = "com.llmbox.description"

	// DefaultImage is launched when the caller does not specify one. Claude is
	// always injected at create time, so this is a plain glibc base rather than a
	// Claude-specific image: it only needs /bin/sh, util-linux (for `script`),
	// and the CA-certificate bundle the box's HTTPS calls rely on. Any glibc
	// image with those works as a substitute; this one is built by Dockerfile.box
	// (debian:bookworm-slim + ca-certificates), since plain debian:bookworm-slim
	// omits ca-certificates and breaks TLS from inside the box.
	DefaultImage = "ghcr.io/clems4ever/llmbox-box:latest"

	// DefaultClaudeBin is where the Dockerfile bakes the standalone Claude binary;
	// it is the fallback source the binary is injected from when no path is set.
	DefaultClaudeBin = "/opt/llmbox/claude"
)

Variables

This section is empty.

Functions

func ValidBoxID added in v0.0.28

func ValidBoxID(id string) bool

ValidBoxID reports whether id is a well-formed box ID (a single DNS hostname label). It is the single source of truth for box-id validation: Create calls it so the local box-creation path validates inputs exactly as the cluster admission policy does on the remote path, rather than relying on the Docker daemon's implicit hostname check to reject a malformed (and potentially shell-injecting) box ID.

@arg id The candidate box ID. @return bool True when id is a valid 1-63 char lowercase hostname label.

@testcase TestValidBoxID accepts well-formed ids and rejects malformed ones.

Types

type Box

type Box struct {
	ContainerID string `json:"container_id" jsonschema:"the short Docker container ID"`
	Name        string `json:"name" jsonschema:"the container name"`
	BoxID       string `json:"box_id,omitempty" jsonschema:"the box ID the caller assigned, if any (also set as the container hostname)"`
	Description string `json:"description,omitempty" jsonschema:"the caller-supplied description label, if any"`
	Spoke       string `json:"spoke,omitempty" jsonschema:"the cluster spoke the box runs on; 'local' for the in-process spoke"`
	Image       string `json:"image" jsonschema:"the image the box runs"`
	State       string `json:"state" jsonschema:"the container state, e.g. running or exited"`
	Status      string `json:"status" jsonschema:"a human readable status string"`
	Phase       string `json:"phase" jsonschema:"auth phase: pending (awaiting login) or ready (authenticated)"`
	Created     int64  `json:"created" jsonschema:"creation time as a unix timestamp"`
}

Box is a view of a managed container returned to callers.

type BoxLimits added in v0.0.28

type BoxLimits struct {
	// MemoryBytes is the hard memory limit per box, in bytes (0 = unlimited).
	MemoryBytes int64
	// NanoCPUs is the CPU quota per box in units of 1e-9 CPUs, i.e. 1_000_000_000
	// is one full CPU (0 = unlimited).
	NanoCPUs int64
	// PidsLimit caps the number of processes/threads in a box, blunting fork
	// bombs (0 = unlimited).
	PidsLimit int64
	// MaxBoxes caps how many managed boxes may exist at once; Create rejects a new
	// box once the count is reached (0 = unlimited).
	MaxBoxes int
}

BoxLimits caps the resources a single box container may consume and the total number of concurrent boxes a Manager will run. It bounds resource-exhaustion (CPU/memory/PID fork-bombs, unbounded box counts) by a caller that can reach the by-design-unauthenticated create/exec path. A zero field means "no limit" for that dimension, so the zero BoxLimits preserves the original unbounded behaviour for a deployment that opts out.

type CreateOptions

type CreateOptions struct {
	// Image is the container image to launch; empty means the Manager default.
	Image string
	// BoxID is the caller-assigned identifier for the box. When set, it is also
	// applied as the box's container hostname (what `hostname` reports inside it,
	// and the name shown in claude.ai/code), so it must be a valid hostname or
	// Docker rejects creation. It must be unique across managed boxes.
	BoxID string
	// Description is a free-form label shown by list/get to help the caller tell
	// boxes apart. It has no effect on the box itself.
	Description string
	// SpokeName selects which cluster spoke the box is created on (empty or
	// "local" means the in-process spoke). It is routing metadata used by the
	// server's cluster layer; the Docker manager itself ignores it.
	SpokeName string
	// Files are written into the box's filesystem after it is created but before
	// it starts, so they are present when the entrypoint runs. Used to inject
	// per-box secrets (e.g. a granular subject token) without baking them into
	// the image, an env var, or a label where `docker inspect` would expose them.
	Files []InjectFile
}

CreateOptions holds the caller-controlled inputs for a new box.

type ExecResult

type ExecResult struct {
	Stdout   string `json:"stdout" jsonschema:"the command's standard output"`
	Stderr   string `json:"stderr" jsonschema:"the command's standard error"`
	ExitCode int    `json:"exit_code" jsonschema:"the command's exit code (0 means success)"`
}

ExecResult is the captured outcome of a command run inside a box.

type InjectFile

type InjectFile struct {
	Path    string
	Content []byte
	Mode    int64
	UID     int
	GID     int
}

InjectFile is one file to write into a new box. Path is absolute inside the container; Content is its bytes; Mode/UID/GID set its permissions and owner (UID/GID matter so a file landing in a non-root user's home stays readable by that user).

type Manager

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

Manager talks to the Docker daemon.

func NewManager

func NewManager(defaultImage, remoteArgs, claudeBin string, peers []string) (*Manager, error)

NewManager creates a Manager using Docker configuration from the environment. defaultImage, remoteArgs, and claudeBin fall back to sensible defaults when empty. claudeBin is the path to the standalone Claude binary that is always injected into each box at creation, which is what lets boxes run on any plain glibc image rather than a Claude-specific one.

@arg defaultImage The image launched when a caller does not specify one; empty falls back to DefaultImage. @arg remoteArgs The remote-control flags; empty falls back to the default flags. @arg claudeBin Path on this host to the standalone Claude binary injected into each box; empty falls back to DefaultClaudeBin. @arg peers Container names (resource servers) connected into every box's own network; empty isolates boxes with no shared peers. @return *Manager A Manager wired to a Docker client built from the environment. @error error if the Docker client cannot be created.

@testcase TestListMapsPhaseFromName covers Manager behaviour via a constructed Manager.

func (*Manager) Close

func (m *Manager) Close() error

Close releases the underlying Docker client.

@error error if the underlying Docker client fails to close.

@testcase TestListMapsPhaseFromName uses a Manager whose lifecycle includes Close.

func (*Manager) Create

func (m *Manager) Create(ctx context.Context, opts CreateOptions) (id, authorizeURL string, err error)

Create creates and starts a box, captures the OAuth authorize URL its login process prints, and returns the container ID plus that URL. The box is left running, parked at the "paste code" prompt, ready for SubmitCode. opts.BoxID is applied as the container hostname, and opts.BoxID/opts.Description are persisted as labels so List can report them. A non-empty opts.BoxID must be a valid hostname label (see ValidBoxID) and unique across managed boxes; the create is rejected otherwise. If the image is not present locally, it is pulled and the create is retried once. Any opts.Files are written into the box after creation but before it starts.

The standalone Claude binary and a ~/.claude.json seed are always injected, the box is forced to run as root with HOME=boxHome and WorkingDir=boxWorkdir, and a node-free entrypoint is used — so the box runs on any plain glibc image without Claude (or Node) baked in. The configured BoxLimits cap the box's memory/CPU/PIDs and the total box count, and the box runs with no-new-privileges. When opts.BoxID is set (and the remote args don't already specify --name), the pre-created first session is named "<box-id>-default" so it is identifiable in claude.ai/code.

@arg ctx Context for the Docker create/start/attach calls. @arg opts The caller-controlled image, box ID, description, and files for the box. @return id The full container ID of the created box. @return authorizeURL The OAuth authorize URL captured from the box's login output. @error error if opts.BoxID is malformed or already in use, the max-box ceiling is reached, the claude binary cannot be read, the image cannot be pulled, or the box cannot be created, files injected, started, or its authorize URL captured.

@testcase TestCreateCapturesURL captures the authorize URL and sets box-id/description labels. @testcase TestCreateCleansUpOnStartFailure removes the container when start fails. @testcase TestCreateRejectsDuplicateBoxID rejects a box ID already in use. @testcase TestCreateRejectsBadBoxID rejects a malformed box ID before creating a container. @testcase TestCreateAppliesBoxLimits applies the configured resource caps and no-new-privileges. @testcase TestCreateAppliesBoxGPUs attaches the configured GPU device requests to the host config. @testcase TestCreateRejectsOverMaxBoxes rejects a create once the box ceiling is reached. @testcase TestCreatePullsMissingImage pulls the image then retries when it is absent. @testcase TestCreateInjectsFiles copies injected files into the box before start. @testcase TestCreateInjectsClaude injects the binary and seed and forces root/HOME/WorkingDir. @testcase TestCreateMissingClaudeBinary fails when the claude binary is unreadable.

func (*Manager) Destroy

func (m *Manager) Destroy(ctx context.Context, idOrName string) error

Destroy gracefully stops and removes a managed box identified by ID or name. It asks the box to stop (SIGTERM to its main process, so Claude can shut down cleanly), waiting up to stopTimeout before Docker escalates to SIGKILL; the stop blocks until the box has terminated. Only then is the container removed.

@arg ctx Context for the Docker stop and remove calls. @arg idOrName The ID or name identifying the box to remove. @error error if no managed box matches, or the container cannot be stopped or removed.

@testcase TestDestroyStopsThenRemoves stops the box before removing it.

func (*Manager) Exec

func (m *Manager) Exec(ctx context.Context, idOrName string, cmd []string) (ExecResult, error)

Exec runs cmd inside a managed box identified by ID or name and returns its captured stdout, stderr, and exit code. The exec runs without a TTY so the two streams stay separable (demultiplexed with stdcopy); each is capped at maxExecOutput. A non-zero exit code is reported in the result, not as an error — only a failure to run the command at all returns an error.

@arg ctx Context for the Docker exec create/attach/inspect calls. @arg idOrName The ID or name identifying the box to run the command in. @arg cmd The command and its arguments to run inside the box. @return ExecResult The command's stdout, stderr, and exit code. @error error if no managed box matches, or the command cannot be created, started, or read.

@testcase TestExecCapturesOutput runs a command and returns its stdout, stderr, and exit code. @testcase TestExecUnknownBox errors when no managed box matches.

func (*Manager) List

func (m *Manager) List(ctx context.Context) ([]Box, error)

List returns all boxes created by this server, running or not.

@arg ctx Context for the Docker list request. @return []Box One Box per managed container, with phase, box ID, and description filled in. @error error if listing containers from Docker fails.

@testcase TestListMapsPhaseFromName checks phase, container ID, box ID, and description mapping.

func (*Manager) Logs

func (m *Manager) Logs(ctx context.Context, idOrName string, tail int) (string, error)

Logs returns the recent console output of a managed box identified by ID or name. tail bounds how many trailing lines are returned; a non-positive tail falls back to defaultLogTail. Boxes run with a TTY, so the log stream is raw (not stdout/stderr multiplexed); the output is ANSI-stripped so the caller gets readable text rather than the TUI's escape sequences.

@arg ctx Context for the Docker logs request. @arg idOrName The ID or name identifying the box to read logs from. @arg tail The maximum number of trailing log lines to return; non-positive uses defaultLogTail. @return string The box's recent console output, ANSI-stripped. @error error if no managed box matches, or the logs cannot be read.

@testcase TestLogsReturnsTail reads a box's logs and strips ANSI from the output. @testcase TestLogsUnknownBox errors when no managed box matches.

func (*Manager) ReapOrphans

func (m *Manager) ReapOrphans(ctx context.Context, ttl time.Duration) ([]string, error)

ReapOrphans destroys pending (never-authenticated) boxes older than ttl. Authenticated ("ready") boxes are never reaped. It returns the IDs reaped.

@arg ctx Context for the underlying list and remove calls. @arg ttl The maximum age a pending box may reach before it is reaped. @return []string The short IDs of the boxes that were reaped. @error error if listing boxes fails.

@testcase TestReapOrphans reaps only old pending boxes, sparing new and ready ones.

func (*Manager) SetBoxGPUs added in v0.0.34

func (m *Manager) SetBoxGPUs(spec string) error

SetBoxGPUs configures the GPUs attached to every box this manager launches, from a spec in the style of `docker run --gpus`: "" attaches none, "all" attaches every GPU, a positive integer attaches that many, and a comma-separated list (optionally "device="-prefixed) selects GPUs by id/index. It is a machine-local setting a spoke sets from its --box-gpus flag.

@arg spec The GPU spec: "", "all", a positive count, or a device list like "device=0,1". @error error if the spec is malformed (e.g. a non-positive or non-numeric count).

@testcase TestSetBoxGPUsParsesSpec accepts all/count/device-list specs and rejects bad ones. @testcase TestCreateAppliesBoxGPUs sets GPUs and checks the device request reaches the host config.

func (*Manager) SetBoxLimits added in v0.0.28

func (m *Manager) SetBoxLimits(l BoxLimits)

SetBoxLimits sets the per-box resource caps and the max concurrent-box count applied by Create. It is called once at startup after NewManager (kept off the constructor so existing callers and tests are unaffected); the zero BoxLimits leaves every dimension unlimited.

@arg l The resource and count limits to enforce on subsequently created boxes.

@testcase TestCreateAppliesBoxLimits sets limits and checks they reach the host config. @testcase TestCreateRejectsOverMaxBoxes rejects a create once MaxBoxes is reached.

func (*Manager) SubmitCode

func (m *Manager) SubmitCode(ctx context.Context, idOrName, code string) (sessionURL string, err error)

SubmitCode writes the OAuth code to a pending box's login prompt, waits for the login to complete and remote-control to print a session URL, then renames the box to mark it authenticated. It returns the session URL exactly as the box printed it (and any tail of output captured, for diagnostics).

@arg ctx Context for the Docker attach call. @arg idOrName The ID or name identifying the pending box. @arg code The OAuth code to write to the box's login prompt. @return sessionURL The remote-control session URL printed once login completes. @error error if no managed box matches, attaching fails, the login does not complete, or the box cannot be renamed to ready.

@testcase TestSubmitCodeReturnsSessionURL writes the code and returns the session URL. @testcase TestSubmitCodeAttachError fails when attaching to the box fails. @testcase TestSubmitCodeUnmanagedBox refuses a container that is not a managed box.

Jump to

Keyboard shortcuts

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