Skip to content
Merged
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
10 changes: 7 additions & 3 deletions cmd/chat_subcommand_sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,19 @@ func (c *sandboxSubcommand) Handle(m *chatModel, args []string, text string) (te
cfg := sandbox.Config{Mode: sandbox.NormalizeMode(mode), Image: os.Getenv("REFLECT_SANDBOX_IMAGE")}
status := "off (host execution)"
if sandbox.Enabled(cfg) {
status = "on (docker)"
status = "on (" + string(cfg.Mode) + ")"
}
docker := "not found"
if sandbox.DockerAvailable() {
docker = "available"
}
msb := "not found"
if sandbox.MicrosandboxAvailable() {
msb = "available"
}
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf(
"Sandbox: %s\nDocker: %s\n\nTo enable, set \"sandbox\": \"docker\" in ~/.reflect/settings.json\nor export REFLECT_SANDBOX=docker (and optionally REFLECT_SANDBOX_IMAGE).",
status, docker,
"Sandbox: %s\nDocker: %s\nMicrosandbox (msb): %s\n\nTo enable, set \"sandbox\": \"docker\" or \"microsandbox\" in ~/.reflect/settings.json\nor export REFLECT_SANDBOX=docker|microsandbox (and optionally REFLECT_SANDBOX_IMAGE).",
status, docker, msb,
)})
return m, nil
}
Expand Down
2 changes: 2 additions & 0 deletions go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 42 additions & 20 deletions internal/sandbox/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import (
type Mode string

const (
ModeOff Mode = "off"
ModeDocker Mode = "docker"
ModeOff Mode = "off"
ModeDocker Mode = "docker"
ModeMicrosandbox Mode = "microsandbox"
)

// Config holds sandbox settings.
Expand All @@ -36,13 +37,14 @@ const DefaultImage = "alpine:latest"
// Available reports whether the configured backend is usable on this machine.
func Available(cfg Config) bool {
switch cfg.Mode {
case ModeDocker, "":
// empty mode treated as "try docker if present"? No — explicit off.
if cfg.Mode == ModeOff {
return true // host is always "available"
}
case ModeOff, "":
return true // host is always "available"
case ModeDocker:
_, err := exec.LookPath("docker")
return err == nil
case ModeMicrosandbox:
_, err := exec.LookPath("msb")
return err == nil
}
return false
}
Expand All @@ -56,7 +58,13 @@ func DockerAvailable() bool {
return exec.CommandContext(context.Background(), "docker", "info").Run() == nil
}

// Run executes command inside a container with workdir bind-mounted at /work.
// MicrosandboxAvailable reports whether the msb CLI is on PATH.
func MicrosandboxAvailable() bool {
_, err := exec.LookPath("msb")
return err == nil
}

// Run executes command inside a sandbox with workdir bind-mounted.
// Returns combined stdout+stderr and an error. Falls back to host execution
// when mode is off.
func Run(ctx context.Context, cfg Config, workdir, command string) (string, error) {
Expand All @@ -65,34 +73,48 @@ func Run(ctx context.Context, cfg Config, workdir, command string) (string, erro
// marker. Return empty to signal "not sandboxed".
return "", fmt.Errorf("sandbox: mode off")
}
if !DockerAvailable() {
return "", fmt.Errorf("sandbox: docker not found on PATH")
}
image := cfg.Image
if image == "" {
image = DefaultImage
}
args := []string{
"run", "--rm",
"-v", workdir + ":/work",
"-w", "/work",
image,
"sh", "-c", command,
switch cfg.Mode {
case ModeDocker:
if !DockerAvailable() {
return "", fmt.Errorf("sandbox: docker not found on PATH")
}
args := []string{
"run", "--rm",
"-v", workdir + ":/work",
"-w", "/work",
image,
"sh", "-c", command,
}
out, err := exec.CommandContext(ctx, "docker", args...).CombinedOutput() // #nosec G204 -- args are controlled (image + bind mount + sh -c)
return string(out), err
case ModeMicrosandbox:
if !MicrosandboxAvailable() {
return "", fmt.Errorf("sandbox: msb (microsandbox) not found on PATH")
}
// microsandbox CLI: msb run <image> -- <command>
args := []string{"run", image, "--", "sh", "-c", command}
out, err := exec.CommandContext(ctx, "msb", args...).CombinedOutput() // #nosec G204 -- args are controlled
return string(out), err
}
out, err := exec.CommandContext(ctx, "docker", args...).CombinedOutput() // #nosec G204 -- args are controlled (image + bind mount + sh -c)
return string(out), err
return "", fmt.Errorf("sandbox: unknown mode %q", cfg.Mode)
}

// Enabled reports whether sandboxing is active.
func Enabled(cfg Config) bool {
return cfg.Mode == ModeDocker
return cfg.Mode == ModeDocker || cfg.Mode == ModeMicrosandbox
}

// NormalizeMode coerces a raw string into a Mode.
func NormalizeMode(raw string) Mode {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "docker", "container":
return ModeDocker
case "microsandbox", "msb", "micro-vm", "microvm":
return ModeMicrosandbox
case "off", "":
return ModeOff
}
Expand Down
13 changes: 13 additions & 0 deletions internal/sandbox/sandbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ func TestRunDockerIfAvailable(t *testing.T) {
}
}

func TestMicrosandboxIfAvailable(t *testing.T) {
if !MicrosandboxAvailable() {
t.Skip("msb not available")
}
out, err := Run(context.Background(), Config{Mode: ModeMicrosandbox, Image: "python"}, os.TempDir(), "echo msb-ok")
if err != nil {
t.Fatalf("msb run failed: %v\n%s", err, out)
}
if !contains(out, "msb-ok") {
t.Fatalf("expected msb-ok in output, got: %s", out)
}
}

func contains(s, sub string) bool {
return len(s) >= len(sub) && func() bool {
for i := 0; i+len(sub) <= len(s); i++ {
Expand Down
Loading