Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,19 @@ DEFAULT_IMAGE=ghcr.io/chaitin/agent-compose-guest:latest
# WORKSPACE_CLEANUP_TTL=0
# IMAGE_CACHE_CLEANUP_TTL=0

# BoxLite runtime storage.
# BoxLite and Microsandbox microVM resources.
# --------------------------------------
# BOXLITE_HOME=/data/boxlite
# BOX_ROOTFS_PATH=
# BOX_DISK_SIZE_GB=6
# SANDBOX_CPUS=4
# Unified BoxLite/Microsandbox default; override for larger workloads.
# SANDBOX_MEMORY_MIB=4096
# SANDBOX_DISK_SIZE_GB=6

# Docker and MicroSandbox storage.
# --------------------------------------
# DOCKER_HOME=/data/docker
# Microsandbox bind mounts get this guest-write quota. Defaults to
# BOX_DISK_SIZE_GB when unset.
# MICROSANDBOX_BIND_QUOTA_GB=6
# Microsandbox bind mounts use SANDBOX_DISK_SIZE_GB as their guest-write quota.
# MICROSANDBOX_INSECURE_REGISTRIES=

# ---------------------------------------------------------------------------
Expand Down
68 changes: 45 additions & 23 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"

Expand All @@ -19,6 +20,12 @@ const DefaultAgentComposeSocketPath = "/var/run/agent-compose.sock"
const DefaultAgentTimeout = 10 * time.Hour
const defaultGuestHomePath = "/root"

const (
DefaultSandboxCPUs uint8 = 4
DefaultSandboxMemoryMiB uint32 = 4096
DefaultSandboxDiskSizeGB int32 = 6
)

const (
RuntimeDriverBoxlite = "boxlite"
RuntimeDriverDocker = "docker"
Expand Down Expand Up @@ -67,14 +74,15 @@ type Config struct {
MicrosandboxLibPath string
MicrosandboxDefaultImage string
MicrosandboxInsecure []string
MicrosandboxBindQuotaGB int
DefaultImage string
BoxRootfsPath string
ImageRegistry string
ImageStoreMode string
ImageCacheRoot string
ImageInsecureRegistries []string
BoxDiskSizeGB int
SandboxCPUs uint8
SandboxMemoryMiB uint32
SandboxDiskSizeGB int32
CacheTTL time.Duration
CleanupInterval time.Duration
WorkspaceCleanupTTL time.Duration
Expand Down Expand Up @@ -273,25 +281,9 @@ func NewConfig(di do.Injector) (*Config, error) {
}
imageInsecureRegistries := splitAndTrimEnv(os.Getenv("IMAGE_INSECURE_REGISTRIES"))

boxDiskSizeGB := 6
if raw := os.Getenv("BOX_DISK_SIZE_GB"); raw != "" {
var parsed int
if _, err := fmt.Sscanf(raw, "%d", &parsed); err != nil || parsed <= 0 {
logger.Warn("failed to parse BOX_DISK_SIZE_GB", "value", raw, "error", err)
} else {
boxDiskSizeGB = parsed
}
}
microsandboxBindQuotaGB := boxDiskSizeGB
if raw := os.Getenv("MICROSANDBOX_BIND_QUOTA_GB"); raw != "" {
var parsed int
if _, err := fmt.Sscanf(raw, "%d", &parsed); err != nil || parsed <= 0 {
logger.Warn("failed to parse MICROSANDBOX_BIND_QUOTA_GB", "value", raw, "error", err)
} else {
microsandboxBindQuotaGB = parsed
}
}

sandboxCPUs := positiveUint8Env(logger, "SANDBOX_CPUS", DefaultSandboxCPUs)
sandboxMemoryMiB := positiveMemoryMiBEnv(logger, "SANDBOX_MEMORY_MIB", DefaultSandboxMemoryMiB)
sandboxDiskSizeGB := positiveDiskSizeGBEnv(logger, "SANDBOX_DISK_SIZE_GB", DefaultSandboxDiskSizeGB)
cacheTTL := 7 * 24 * time.Hour
cacheTTLRaw, err := envWithLegacy(logger, "CACHE_TTL", "BOX_CACHE_TTL")
if err != nil {
Expand Down Expand Up @@ -483,14 +475,15 @@ func NewConfig(di do.Injector) (*Config, error) {
MicrosandboxLibPath: microsandboxLibPath,
MicrosandboxDefaultImage: microsandboxDefaultImage,
MicrosandboxInsecure: microsandboxInsecure,
MicrosandboxBindQuotaGB: microsandboxBindQuotaGB,
DefaultImage: defaultImage,
BoxRootfsPath: boxRootfsPath,
ImageRegistry: imageRegistry,
ImageStoreMode: imageStoreMode,
ImageCacheRoot: imageCacheRoot,
ImageInsecureRegistries: imageInsecureRegistries,
BoxDiskSizeGB: boxDiskSizeGB,
SandboxCPUs: sandboxCPUs,
SandboxMemoryMiB: sandboxMemoryMiB,
SandboxDiskSizeGB: sandboxDiskSizeGB,
CacheTTL: cacheTTL,
CleanupInterval: cleanupInterval,
WorkspaceCleanupTTL: workspaceCleanupTTL,
Expand Down Expand Up @@ -815,3 +808,32 @@ func splitAndTrimEnv(value string) []string {
}
return items
}

func positiveUint8Env(logger *slog.Logger, name string, defaultValue uint8) uint8 {
return uint8(positiveUintEnv(logger, name, uint64(defaultValue), 8))
}

func positiveMemoryMiBEnv(logger *slog.Logger, name string, defaultValue uint32) uint32 {
// BoxLite accepts memory through a signed C int, so the shared value must
// fit both that API and Microsandbox's uint32 option.
return uint32(positiveUintEnv(logger, name, uint64(defaultValue), 31))
}

func positiveDiskSizeGBEnv(logger *slog.Logger, name string, defaultValue int32) int32 {
// Microsandbox exposes the bind quota in MiB as uint32. Restrict the GiB
// value so multiplying it by 1024 cannot wrap.
return int32(positiveUintEnv(logger, name, uint64(defaultValue), 22))
}

func positiveUintEnv(logger *slog.Logger, name string, defaultValue uint64, bitSize int) uint64 {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return defaultValue
}
parsed, err := strconv.ParseUint(raw, 10, bitSize)
if err != nil || parsed == 0 {
logger.Warn("failed to parse positive integer environment variable", "name", name, "value", raw, "error", err)
return defaultValue
}
return parsed
}
103 changes: 35 additions & 68 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func testNewConfigParsesEnvironment(t *testing.T) {
t.Setenv("IMAGE_STORE_MODE", "oci")
t.Setenv("IMAGE_CACHE_ROOT", filepath.Join(root, "custom-images"))
t.Setenv("IMAGE_INSECURE_REGISTRIES", "oci-one.example; oci-two.example\noci-three.example")
t.Setenv("BOX_DISK_SIZE_GB", "11")
t.Setenv("SANDBOX_DISK_SIZE_GB", "11")
t.Setenv("CACHE_TTL", "2h")
t.Setenv("CLEANUP_INTERVAL", "30m")
t.Setenv("WORKSPACE_CLEANUP_TTL", "24h")
Expand Down Expand Up @@ -173,8 +173,8 @@ func testNewConfigParsesEnvironment(t *testing.T) {
if config.LLMTimeout != 7*time.Second || config.AgentTimeout != 8*time.Second {
t.Fatalf("timeouts = %s/%s", config.LLMTimeout, config.AgentTimeout)
}
if config.BoxDiskSizeGB != 11 || config.CacheTTL != 2*time.Hour {
t.Fatalf("box disk/cache = %d/%s", config.BoxDiskSizeGB, config.CacheTTL)
if config.SandboxDiskSizeGB != 11 || config.CacheTTL != 2*time.Hour {
t.Fatalf("sandbox disk/cache = %d/%s", config.SandboxDiskSizeGB, config.CacheTTL)
}
if config.CleanupInterval != 30*time.Minute || config.WorkspaceCleanupTTL != 24*time.Hour || config.ImageCacheCleanupTTL != 48*time.Hour {
t.Fatalf("cleanup config = %s/%s/%s", config.CleanupInterval, config.WorkspaceCleanupTTL, config.ImageCacheCleanupTTL)
Expand Down Expand Up @@ -865,7 +865,7 @@ func testDefaultDataRootFallsBackToHome(t *testing.T) {
}
}

func TestBoxDiskSizeGB(t *testing.T) {
func TestBoxResources(t *testing.T) {
newCfg := func(t *testing.T) *Config {
t.Helper()
root := t.TempDir()
Expand All @@ -879,91 +879,58 @@ func TestBoxDiskSizeGB(t *testing.T) {
return cfg
}

t.Run("default is 6 for all VM-type drivers", func(t *testing.T) {
t.Run("defaults", func(t *testing.T) {
cfg := newCfg(t)
if cfg.BoxDiskSizeGB != 6 {
t.Fatalf("BoxDiskSizeGB = %d, want 6 (default)", cfg.BoxDiskSizeGB)
if cfg.SandboxCPUs != 4 || cfg.SandboxMemoryMiB != 4096 || cfg.SandboxDiskSizeGB != 6 {
t.Fatalf("sandbox resources = %d CPUs/%d MiB/%d GiB, want 4/4096/6", cfg.SandboxCPUs, cfg.SandboxMemoryMiB, cfg.SandboxDiskSizeGB)
}
})

t.Run("BOX_DISK_SIZE_GB sets the value", func(t *testing.T) {
t.Setenv("BOX_DISK_SIZE_GB", "11")
t.Run("environment sets shared resources", func(t *testing.T) {
t.Setenv("SANDBOX_CPUS", "8")
t.Setenv("SANDBOX_MEMORY_MIB", "16384")
t.Setenv("SANDBOX_DISK_SIZE_GB", "11")
cfg := newCfg(t)
if cfg.BoxDiskSizeGB != 11 {
t.Fatalf("BoxDiskSizeGB = %d, want 11", cfg.BoxDiskSizeGB)
if cfg.SandboxCPUs != 8 || cfg.SandboxMemoryMiB != 16384 || cfg.SandboxDiskSizeGB != 11 {
t.Fatalf("sandbox resources = %d CPUs/%d MiB/%d GiB, want 8/16384/11", cfg.SandboxCPUs, cfg.SandboxMemoryMiB, cfg.SandboxDiskSizeGB)
}
})

t.Run("invalid value keeps the default", func(t *testing.T) {
t.Setenv("BOX_DISK_SIZE_GB", "abc")
cfg := newCfg(t)
if cfg.BoxDiskSizeGB != 6 {
t.Fatalf("BoxDiskSizeGB = %d, want 6", cfg.BoxDiskSizeGB)
}
})

t.Run("non-positive value keeps the default", func(t *testing.T) {
t.Setenv("BOX_DISK_SIZE_GB", "-3")
cfg := newCfg(t)
if cfg.BoxDiskSizeGB != 6 {
t.Fatalf("BoxDiskSizeGB = %d, want 6", cfg.BoxDiskSizeGB)
}
})
}

func TestMicrosandboxBindQuotaGB(t *testing.T) {
newCfg := func(t *testing.T) *Config {
t.Helper()
root := t.TempDir()
t.Setenv("DATA_ROOT", filepath.Join(root, "data"))
di := do.New()
do.ProvideValue(di, slog.Default())
cfg, err := NewConfig(di)
if err != nil {
t.Fatalf("NewConfig returned error: %v", err)
}
return cfg
}

t.Run("defaults to box disk size", func(t *testing.T) {
t.Run("SANDBOX_DISK_SIZE_GB sets the value", func(t *testing.T) {
t.Setenv("SANDBOX_DISK_SIZE_GB", "11")
cfg := newCfg(t)
if cfg.MicrosandboxBindQuotaGB != 6 {
t.Fatalf("MicrosandboxBindQuotaGB = %d, want 6", cfg.MicrosandboxBindQuotaGB)
if cfg.SandboxDiskSizeGB != 11 {
t.Fatalf("SandboxDiskSizeGB = %d, want 11", cfg.SandboxDiskSizeGB)
}
})

t.Run("follows BOX_DISK_SIZE_GB", func(t *testing.T) {
t.Setenv("BOX_DISK_SIZE_GB", "60")
cfg := newCfg(t)
if cfg.MicrosandboxBindQuotaGB != 60 {
t.Fatalf("MicrosandboxBindQuotaGB = %d, want 60", cfg.MicrosandboxBindQuotaGB)
}
})

t.Run("override sets the value", func(t *testing.T) {
t.Setenv("BOX_DISK_SIZE_GB", "60")
t.Setenv("MICROSANDBOX_BIND_QUOTA_GB", "96")
t.Run("invalid value keeps the default", func(t *testing.T) {
t.Setenv("SANDBOX_CPUS", "abc")
t.Setenv("SANDBOX_MEMORY_MIB", "abc")
t.Setenv("SANDBOX_DISK_SIZE_GB", "abc")
cfg := newCfg(t)
if cfg.MicrosandboxBindQuotaGB != 96 {
t.Fatalf("MicrosandboxBindQuotaGB = %d, want 96", cfg.MicrosandboxBindQuotaGB)
if cfg.SandboxCPUs != 4 || cfg.SandboxMemoryMiB != 4096 || cfg.SandboxDiskSizeGB != 6 {
t.Fatalf("sandbox resources = %d CPUs/%d MiB/%d GiB, want defaults", cfg.SandboxCPUs, cfg.SandboxMemoryMiB, cfg.SandboxDiskSizeGB)
}
})

t.Run("invalid override keeps the inherited default", func(t *testing.T) {
t.Setenv("BOX_DISK_SIZE_GB", "60")
t.Setenv("MICROSANDBOX_BIND_QUOTA_GB", "abc")
t.Run("non-positive value keeps the default", func(t *testing.T) {
t.Setenv("SANDBOX_CPUS", "0")
t.Setenv("SANDBOX_MEMORY_MIB", "-1")
t.Setenv("SANDBOX_DISK_SIZE_GB", "-3")
cfg := newCfg(t)
if cfg.MicrosandboxBindQuotaGB != 60 {
t.Fatalf("MicrosandboxBindQuotaGB = %d, want 60", cfg.MicrosandboxBindQuotaGB)
if cfg.SandboxCPUs != 4 || cfg.SandboxMemoryMiB != 4096 || cfg.SandboxDiskSizeGB != 6 {
t.Fatalf("sandbox resources = %d CPUs/%d MiB/%d GiB, want defaults", cfg.SandboxCPUs, cfg.SandboxMemoryMiB, cfg.SandboxDiskSizeGB)
}
})

t.Run("non-positive override keeps the inherited default", func(t *testing.T) {
t.Setenv("BOX_DISK_SIZE_GB", "60")
t.Setenv("MICROSANDBOX_BIND_QUOTA_GB", "0")
t.Run("out-of-range value keeps the default", func(t *testing.T) {
t.Setenv("SANDBOX_CPUS", "256")
t.Setenv("SANDBOX_MEMORY_MIB", "2147483648")
t.Setenv("SANDBOX_DISK_SIZE_GB", "4194304")
cfg := newCfg(t)
if cfg.MicrosandboxBindQuotaGB != 60 {
t.Fatalf("MicrosandboxBindQuotaGB = %d, want 60", cfg.MicrosandboxBindQuotaGB)
if cfg.SandboxCPUs != 4 || cfg.SandboxMemoryMiB != 4096 || cfg.SandboxDiskSizeGB != 6 {
t.Fatalf("sandbox resources = %d CPUs/%d MiB/%d GiB, want defaults", cfg.SandboxCPUs, cfg.SandboxMemoryMiB, cfg.SandboxDiskSizeGB)
}
})
}
7 changes: 4 additions & 3 deletions pkg/driver/boxlite_cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -1002,9 +1002,10 @@ func (r *cgoSandboxRuntime) buildBoxOptions(ctx context.Context, sandbox *Sandbo
C.boxlite_options_set_auto_remove(options, 0)
C.boxlite_options_set_detach(options, 1)
C.boxlite_options_set_network_enabled(options)
if r.config.BoxDiskSizeGB > 0 {
C.boxlite_options_set_disk_size_gb(options, C.int(r.config.BoxDiskSizeGB))
}
resources := configuredSandboxResources(r.config)
C.boxlite_options_set_cpus(options, C.int(resources.CPUs))
C.boxlite_options_set_memory(options, C.int(resources.MemoryMiB))
C.boxlite_options_set_disk_size_gb(options, C.int(resources.DiskSizeGB))

for _, mount := range manifest.Mounts {
hostPathCString := C.CString(mount.HostPath)
Expand Down
29 changes: 9 additions & 20 deletions pkg/driver/microsandbox_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -719,12 +719,9 @@ func (r *microsandboxRuntime) ensureDockerDisk(sandboxID string) (string, error)
}
}
// Create a sparse raw file then format it as ext4. Sized by
// BOX_DISK_SIZE_GB (shared with the boxlite driver; config default 6 GiB).
// SANDBOX_DISK_SIZE_GB (shared with the BoxLite driver; default 6 GiB).
// Existing .raw files are reused as-is (see os.Stat check above).
sizeGB := r.config.BoxDiskSizeGB
if sizeGB <= 0 {
sizeGB = 6 // defensive: config normally guarantees a positive value
}
sizeGB := configuredSandboxResources(r.config).DiskSizeGB
f, err := os.Create(path)
if err != nil {
return "", fmt.Errorf("create docker disk image %s: %w", path, err)
Expand Down Expand Up @@ -941,8 +938,8 @@ func (r *microsandboxRuntime) createSandbox(ctx context.Context, session *Sandbo
"guest_path", mount.GuestPath,
"readonly", mount.ReadOnly,
"quota_mib", bindMount.QuotaMiB,
"configured_bind_quota_gb", r.config.MicrosandboxBindQuotaGB,
"box_disk_size_gb", r.config.BoxDiskSizeGB,
"configured_bind_quota_gb", r.config.SandboxDiskSizeGB,
"sandbox_disk_size_gb", r.config.SandboxDiskSizeGB,
)
}
// Give docker its own disk-backed ext4 volume. The guest root is virtiofs,
Expand Down Expand Up @@ -1014,6 +1011,7 @@ func (r *microsandboxRuntime) createSandbox(ctx context.Context, session *Sandbo
rebindDisabled := false
network := microsandbox.NetworkPolicy.AllowAll()
network.DNS = &microsandbox.DNSConfig{RebindProtection: &rebindDisabled}
resources := configuredSandboxResources(r.config)
options := []microsandbox.SandboxOption{
microsandbox.WithImage(imageRef),
microsandbox.WithWorkdir("/"),
Expand All @@ -1023,11 +1021,8 @@ func (r *microsandboxRuntime) createSandbox(ctx context.Context, session *Sandbo
microsandbox.WithPullPolicy(pullPolicy),
microsandbox.WithMounts(mounts),
microsandbox.WithLabels(map[string]string{microsandboxManagedLabel: "true", microsandboxSandboxIDLabel: session.Summary.ID}),
// Fixed microVM size: the SDK defaults (512MiB / 1 CPU) are too small
// for docker-in-VM workloads (pulling large images, building from a
// container).
microsandbox.WithMemory(8192),
microsandbox.WithCPUs(4),
microsandbox.WithMemory(resources.MemoryMiB),
Comment thread
eetoc marked this conversation as resolved.
microsandbox.WithCPUs(resources.CPUs),
}
if jupyterEnabled(proxyState) && proxyState.HostPort > 0 {
options = append(options, microsandbox.WithPorts(map[uint16]uint16{uint16(proxyState.HostPort): uint16(proxyState.GuestPort)}))
Expand All @@ -1048,14 +1043,8 @@ func (r *microsandboxRuntime) microsandboxBindMount(hostPath string, readonly bo
return mount
}

func (r *microsandboxRuntime) microsandboxBindQuotaGB() int {
if r.config == nil {
return 0
}
if r.config.MicrosandboxBindQuotaGB > 0 {
return r.config.MicrosandboxBindQuotaGB
}
return r.config.BoxDiskSizeGB
func (r *microsandboxRuntime) microsandboxBindQuotaGB() int32 {
return configuredSandboxResources(r.config).DiskSizeGB
}

func (r *microsandboxRuntime) resolveMicrosandboxImageRef(ctx context.Context, imageRef, pullPolicy string, pullTimeout time.Duration) (string, []string, bool, error) {
Expand Down
Loading
Loading