sand

package module
v0.0.0-...-403a488 Latest Latest
Warning

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

Go to latest
Published: Jan 28, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

README

Go Reference Main Commit Queue

If you don't write code on a Mac, and you don't deploy it to Linux, you can stop reading this now.

TL;DR

% go install github.com/banksean/sand/cmd/sand
% sand new your-new-sandbox-name

You are now root, in a Linux container, with your CWD set to a copy-on-write clone of the directory you were at when you ran sand on your MacOS host.

You work with a sandboxed clone of ./

The sandbox starts out with a clone of your MacOS current directory, mounted as /app inside the container.

This operation actually uses much less disk space than a full copy of the original directory, because sand clones it using copy-on-write (via APFS's clonefile(2) call). Additional disk space is only required when you make changes to the cloned files.

The original files on your MacOS host filesystem are not affected by changes made to the clones of those files inside the sandbox.

Getting changes out of the sandbox

You can use git commands to push changes from the container to github, or wherever your origin is.

Git ssh authentication passes from your MacOS host through sand containers, via ssh-agent. This means that if the git checkout on your MacOS host is authenticated with ssh (git remote -v origin prints something that starts with [email protected]:...), then you don't need to log in again inside the container to make git push/pull to work.

Using ssh-agent also means you don't have to leave copies of your github credentials scattered around in places where they shouldn't be.

See cmd/sand/GIT_REMOTES.md for a more detailed explanation of how sand uses git locally to link the MacOS-side clones back to your original working directory. If you are a git enthusiast, you can probably figure out how move changes around between your MacOS host and your sandbox containers without involving github at all.

Some other handy commands

$ sand --help # a much more complete list of commands and flags
% sand ls # lists your current sandboxes
% sand git status your-sandbox-name # prints the results of running "git status" in the sandbox's working directory
% sand git diff your-sandbox-name # compares your working directory to the sandbox's clone of your working directory
% sand vsc your-sandbox-name # launches a vscode window, connected "remotely" to your-sandbox-name
% sand shell your-sandbox-name # open a new shell into the your-sandbox-name's container
% sand stop your-sanbox-name # stops the sandbox container, but does *not* delete its filesystem
% sand rm your-sandbox-name # stops and removes the container, and *does* remove the sandbox's filesystem.

For more information about sand's subcommands and other options, see cmd/sand/HELP.md

Requirements

  • Only works on Apple hardware (of course).
  • Install apple/container first, since these helper functions just shell out to it.

Documentation

Index

Constants

View Source
const (
	ClonedWorkDirGitRemotePrefix = "sand/"
)
View Source
const (
	DefaultImageName = "ghcr.io/banksean/sand/default:latest"
)

Variables

This section is empty.

Functions

func EnsureDaemon

func EnsureDaemon(ctx context.Context, appBaseDir, logFile string) error

Types

type Box

type Box struct {
	// ID is an opaque identifier for the sandbox
	ID string
	// ContainerID is the ID of the container
	ContainerID string
	// HostOriginDir is the origin of the sandbox, from which we clone its contents
	HostOriginDir string
	// SandboxWorkDir is the host OS filesystem path containing the sandbox's c-o-w clone of hostOriginDir.
	SandboxWorkDir string
	// ImageName is the name of the container image
	ImageName string
	// DNSDomain is the dns domain for the sandbox's network
	DNSDomain string
	// EnvFile is the host filesystem path to the env file to use when executing commands in the container
	EnvFile string
	// Mounts defines bind mounts that should be attached when creating the container.
	Mounts []MountSpec
	// SandboxWorkDirError and SandboxContainerError are the most recently updated error states of the sandbox
	// work dir and container instance. In-memory only. Updated once either at
	// server startup or sandbox creation time, and then updated periodically thereafter.
	// Empty string implies things are ok.
	// TODO: Make sandbox operations conditional on these values, so that e.g. you don't try to start
	// a sandbox container instance if the sandbox's work dir is not available.
	SandboxWorkDirError   string
	SandboxContainerError string
	// ContainerHooks run after the container has started to perform any bootstrap logic.
	ContainerHooks []ContainerStartupHook `json:"-"`
	Keys           *sshimmer.Keys
	// contains filtered or unexported fields
}

Box is a "sandbox" - it represents the connection between - a local filesystem clone of a local dev workspace directory - a local container instance (whose state is managed by a separate container service)

At startup, the sand.Mux server will synchronize its internal database with the current observed state of the local filesystem clone root and the local container service.

func (*Box) CreateContainer

func (sb *Box) CreateContainer(ctx context.Context) error

CreateContainer creates a new container instance. The container image must exist.

func (*Box) Exec

func (sb *Box) Exec(ctx context.Context, shellCmd string, args ...string) (string, error)

Exec executes a command in the container. The container must be in state "running".

func (*Box) GetContainer

func (sb *Box) GetContainer(ctx context.Context) (*types.Container, error)

func (*Box) Shell

func (sb *Box) Shell(ctx context.Context, env map[string]string, shellCmd string, stdin io.Reader, stdout, stderr io.Writer) error

Shell executes a command in the container. The container must be in state "running".

func (*Box) StartContainer

func (sb *Box) StartContainer(ctx context.Context) error

StartContainer starts a container instance. The container must exist, and it should not be in the "running" state.

func (*Box) Sync

func (sb *Box) Sync(ctx context.Context) error

type Boxer

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

Boxer manages the lifecycle of sandboxes.

func NewBoxer

func NewBoxer(appRoot string, terminalWriter io.Writer) (*Boxer, error)

func (*Boxer) AttachSandbox

func (sb *Boxer) AttachSandbox(ctx context.Context, id string) (*Box, error)

AttachSandbox re-connects to an existing container and sandboxWorkDir instead of creating a new one.

func (*Boxer) Cleanup

func (sb *Boxer) Cleanup(ctx context.Context, sbox *Box) error

func (*Boxer) Close

func (sb *Boxer) Close() error

func (*Boxer) EnsureImage

func (sb *Boxer) EnsureImage(ctx context.Context, imageName string) error

EnsureImage makes sure the requested container image is present locally, pulling it if required.

func (*Boxer) Get

func (sb *Boxer) Get(ctx context.Context, id string) (*Box, error)

func (*Boxer) List

func (sb *Boxer) List(ctx context.Context) ([]Box, error)

func (*Boxer) NewSandbox

func (sb *Boxer) NewSandbox(ctx context.Context, cloner WorkspaceCloner, id, hostWorkDir, imageName, envFile string) (*Box, error)

NewSandbox creates a new sandbox based on a clone of hostWorkDir. TODO: clone envFile, if it exists, into the sandbox clone so every command exec'd in that sandbox container uses the same env file, even if the original .env file has changed on the host machine.

func (*Boxer) SaveSandbox

func (sb *Boxer) SaveSandbox(ctx context.Context, sbox *Box) error

SaveSandbox persists the Sandbox to the database.

func (*Boxer) StopContainer

func (sb *Boxer) StopContainer(ctx context.Context, sbox *Box) error

StopContainer stops a sandbox's container without deleting it.

func (*Boxer) Sync

func (sb *Boxer) Sync(ctx context.Context) error

Sync tells Boxer to synchronize its internal database with the external states of the clone tool directory and local container service.

func (*Boxer) UpdateContainerID

func (sb *Boxer) UpdateContainerID(ctx context.Context, sbox *Box, containerID string) error

UpdateContainerID updates the ContainerID field of a sandbox and persists it.

type ClaudeWorkspaceCloner

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

func (*ClaudeWorkspaceCloner) Hydrate

func (c *ClaudeWorkspaceCloner) Hydrate(ctx context.Context, box *Box) error

func (*ClaudeWorkspaceCloner) Prepare

type CloneRequest

type CloneRequest struct {
	ID          string
	HostWorkDir string
	EnvFile     string
}

CloneRequest captures the inputs necessary to prepare a sandbox workspace.

type CloneResult

type CloneResult struct {
	SandboxWorkDir string
	Mounts         []MountSpec
	ContainerHooks []ContainerStartupHook
}

CloneResult describes the assets created for a sandbox and how to mount/configure them.

type ContainerOps

type ContainerOps interface {
	Create(ctx context.Context, opts *options.CreateContainer, image string, args []string) (string, error)
	Start(ctx context.Context, opts *options.StartContainer, containerID string) (string, error)
	Stop(ctx context.Context, opts *options.StopContainer, containerID string) (string, error)
	Delete(ctx context.Context, opts *options.DeleteContainer, containerID string) (string, error)
	Exec(ctx context.Context, opts *options.ExecContainer, containerID, cmd string, env []string, args ...string) (string, error)
	ExecStream(ctx context.Context, opts *options.ExecContainer, containerID, cmd string, env []string, stdin io.Reader, stdout, stderr io.Writer) (func() error, error)
	Inspect(ctx context.Context, containerID string) ([]types.Container, error)
}

func NewAppleContainerOps

func NewAppleContainerOps() ContainerOps

type ContainerStartupHook

type ContainerStartupHook interface {
	Name() string
	OnStart(ctx context.Context, b *Box) error
}

ContainerStartupHook allows callers to inject container startup customisation.

func NewContainerStartupHook

func NewContainerStartupHook(name string, fn func(ctx context.Context, b *Box) error) ContainerStartupHook

NewContainerStartupHook helpers callers construct hook instances without exporting internals.

type CreateSandboxOpts

type CreateSandboxOpts struct {
	ID           string `json:"id,omitempty"`
	CloneFromDir string `json:"cloneFromDir,omitempty"`
	ImageName    string `json:"imageName,omitempty"`
	EnvFile      string `json:"envFile,omitempty"`
	Cloner       string `json:"cloner,omitempty"`
}

type DefaultWorkspaceCloner

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

DefaultWorkspaceCloner reproduces the current cloning behaviour.

func (*DefaultWorkspaceCloner) Hydrate

func (p *DefaultWorkspaceCloner) Hydrate(ctx context.Context, box *Box) error

Hydrate populates runtime-only fields on a Box that was loaded from persistent storage.

func (*DefaultWorkspaceCloner) Prepare

type FileOps

type FileOps interface {
	MkdirAll(path string, perm os.FileMode) error
	Copy(ctx context.Context, src, dst string) error
	Stat(path string) (os.FileInfo, error)
	Lstat(path string) (os.FileInfo, error)
	Readlink(path string) (string, error)
	Create(path string) (*os.File, error)
	RemoveAll(path string) error
	WriteFile(path string, data []byte, perm os.FileMode) error
}

func NewDefaultFileOps

func NewDefaultFileOps() FileOps

type GitOps

type GitOps interface {
	AddRemote(ctx context.Context, dir, name, url string) error
	RemoveRemote(ctx context.Context, dir, name string) error
	Fetch(ctx context.Context, dir, remote string) error
	TopLevel(ctx context.Context, dir string) string
}

func NewDefaultGitOps

func NewDefaultGitOps() GitOps

type HostMCP

type HostMCP struct {
	ChromeDevToolsPort int
	ChromeUserDataDir  string // e.g. /tmp/chrome-profile-stable
	// contains filtered or unexported fields
}

func (*HostMCP) Cleanup

func (m *HostMCP) Cleanup(ctx context.Context) error

func (*HostMCP) StartHostServices

func (m *HostMCP) StartHostServices(ctx context.Context) error

StartHostServices starts MCP-supporting processes on the host machine. Since these processes are not specific to a particular sandbox, they are shared across sandboxes.

type ImageOps

type ImageOps interface {
	List(ctx context.Context) ([]types.ImageEntry, error)
	Pull(ctx context.Context, image string) (func() error, error)
}

func NewAppleImageOps

func NewAppleImageOps() ImageOps

type MountSpec

type MountSpec struct {
	Source   string
	Target   string
	ReadOnly bool
}

MountSpec describes a bind mount that should be attached to a container.

func (MountSpec) String

func (m MountSpec) String() string

String renders the mount specification into the container runtime format.

type Mux

type Mux struct {
	AppBaseDir string
	SocketPath string
	// contains filtered or unexported fields
}

func NewMuxServer

func NewMuxServer(appBaseDir string, sber *Boxer) *Mux

func (*Mux) CreateSandbox

func (m *Mux) CreateSandbox(ctx context.Context, opts CreateSandboxOpts) (*Box, error)

CreateSandbox creates a new sandbox and starts its container.

func (*Mux) GetSandbox

func (m *Mux) GetSandbox(ctx context.Context, id string) (*Box, error)

GetSandbox retrieves a sandbox by ID.

func (*Mux) ListSandboxes

func (m *Mux) ListSandboxes(ctx context.Context) ([]Box, error)

ListSandboxes returns all sandboxes.

func (*Mux) NewClient

func (m *Mux) NewClient(ctx context.Context) (*MuxClient, error)

func (*Mux) RemoveSandbox

func (m *Mux) RemoveSandbox(ctx context.Context, id string) error

RemoveSandbox removes a single sandbox.

func (*Mux) ServeUnixSocket

func (m *Mux) ServeUnixSocket(ctx context.Context) error

ServeUnixSocket serves the unix domain socket that sandmux clients (the CLI, e.g.) connect to.

func (*Mux) Shutdown

func (m *Mux) Shutdown(ctx context.Context)

func (*Mux) StopSandbox

func (m *Mux) StopSandbox(ctx context.Context, id string) error

StopSandbox stops a single sandbox container.

type MuxClient

type MuxClient struct {
	Mux *Mux
	// contains filtered or unexported fields
}

func (*MuxClient) CreateSandbox

func (m *MuxClient) CreateSandbox(ctx context.Context, opts CreateSandboxOpts) (*Box, error)

func (*MuxClient) GetSandbox

func (m *MuxClient) GetSandbox(ctx context.Context, id string) (*Box, error)

func (*MuxClient) ListSandboxes

func (m *MuxClient) ListSandboxes(ctx context.Context) ([]Box, error)

func (*MuxClient) Ping

func (m *MuxClient) Ping(ctx context.Context) error

func (*MuxClient) RemoveSandbox

func (m *MuxClient) RemoveSandbox(ctx context.Context, id string) error

func (*MuxClient) Shutdown

func (m *MuxClient) Shutdown(ctx context.Context) error

func (*MuxClient) StopSandbox

func (m *MuxClient) StopSandbox(ctx context.Context, id string) error

func (*MuxClient) Version

func (m *MuxClient) Version(ctx context.Context) (version.Info, error)

type OpenCodeWorkspaceCloner

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

func (*OpenCodeWorkspaceCloner) Hydrate

func (c *OpenCodeWorkspaceCloner) Hydrate(ctx context.Context, box *Box) error

func (*OpenCodeWorkspaceCloner) Prepare

type UserMessenger

type UserMessenger interface {
	Message(ctx context.Context, msg string)
}

func NewNullMessenger

func NewNullMessenger() UserMessenger

func NewTerminalMessenger

func NewTerminalMessenger(writer io.Writer) UserMessenger

type WorkspaceCloner

type WorkspaceCloner interface {
	Prepare(ctx context.Context, req CloneRequest) (*CloneResult, error)
	// BUG: Hydrate isn't getting called anywhere, so extra container startup hooks aren't getting registered or invoked.
	Hydrate(ctx context.Context, box *Box) error
}

WorkspaceCloner abstracts the steps for preparing sandbox host resources.

func NewClaudeWorkspaceCloner

func NewClaudeWorkspaceCloner(baseCloner WorkspaceCloner, appRoot string, terminalWriter io.Writer) WorkspaceCloner

func NewDefaultWorkspaceCloner

func NewDefaultWorkspaceCloner(appRoot string, terminalWriter io.Writer) WorkspaceCloner

NewDefaultWorkspaceCloner constructs the default provisioner used by Boxer.

func NewOpenCodeWorkspaceCloner

func NewOpenCodeWorkspaceCloner(baseCloner WorkspaceCloner, appRoot string, terminalWriter io.Writer) WorkspaceCloner

Directories

Path Synopsis
package applecontainer implements helper functions and types for use with apple's container library.
package applecontainer implements helper functions and types for use with apple's container library.
options
package options defines structs for the flagsets passed to various `container` commands.
package options defines structs for the flagsets passed to various `container` commands.
types
package types defines structs for unmashaling the output from various `container` commands.
package types defines structs for unmashaling the output from various `container` commands.
cmd
sand command

Jump to

Keyboard shortcuts

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