diff --git a/cmd/chat_subcommand_sandbox.go b/cmd/chat_subcommand_sandbox.go index f1944ac1..e6fc44aa 100644 --- a/cmd/chat_subcommand_sandbox.go +++ b/cmd/chat_subcommand_sandbox.go @@ -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 } diff --git a/go.mod b/go.mod index a387f4a1..c3ef914b 100644 --- a/go.mod +++ b/go.mod @@ -100,3 +100,5 @@ require ( modernc.org/memory v1.11.0 // indirect ) + + diff --git a/internal/sandbox/sandbox.go b/internal/sandbox/sandbox.go index 4afeba80..373a17ea 100644 --- a/internal/sandbox/sandbox.go +++ b/internal/sandbox/sandbox.go @@ -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. @@ -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 } @@ -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) { @@ -65,27 +73,39 @@ 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 -- + 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. @@ -93,6 +113,8 @@ 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 } diff --git a/internal/sandbox/sandbox_test.go b/internal/sandbox/sandbox_test.go index e70532b2..c2bbdc74 100644 --- a/internal/sandbox/sandbox_test.go +++ b/internal/sandbox/sandbox_test.go @@ -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++ {