Skip to content
Open
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
7 changes: 3 additions & 4 deletions cmd/cli/commands/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ func newConfigureCmd() *cobra.Command {
var flags ConfigureFlags

c := &cobra.Command{
Use: "configure [--context-size=<n>] [--speculative-draft-model=<model>] [--hf_overrides=<json>] [--gpu-memory-utilization=<float>] [--mode=<mode>] [--think] [--keep-alive=<duration>] MODEL [-- <runtime-flags...>]",
Aliases: []string{"config"},
Short: "Manage model runtime configurations",
Hidden: true,
Use: "configure [--context-size=<n>] [--speculative-draft-model=<model>] [--hf_overrides=<json>] [--gpu-memory-utilization=<float>] [--mode=<mode>] [--think] [--keep-alive=<duration>] MODEL [-- <runtime-flags...>]",
Short: "Manage model runtime configurations",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping this wouldn't be removed we could keep this like this without removing if not it would break the style of dmr config or docker model config style .

What i suggest is we register it as a sub command in config. so one can do dmr config sandbox.tool ... or docker model config sandbox.tool

like c.AddCommand(newSandboxConfigCmd()) (note that sandbox.tool is not lke a cmd style format but not sure in this case there is any other approach)

Hidden: true,
Args: func(cmd *cobra.Command, args []string) error {
argsBeforeDash := cmd.ArgsLenAtDash()
if argsBeforeDash == -1 {
Expand Down
44 changes: 44 additions & 0 deletions cmd/cli/commands/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,23 @@ Examples:
appArgs = args[dashIdx:]
}

// If a sandbox tool is configured, launch host apps through it before
// resolving runner endpoints. This keeps sandbox launch independent of
// whether the host app binary itself is installed.
if _, ok := hostApps[app]; ok {
sandboxTool, err := readSandboxToolConfig()
if err != nil {
return err
}

if sandboxTool != "" {
if err := validateSandboxTool(sandboxTool); err != nil {
return err
}
return launchSandboxedHostApp(cmd, sandboxTool, app, appArgs, dryRun)
}
}

runner, err := getStandaloneRunner(cmd.Context())
if err != nil {
return fmt.Errorf("unable to determine standalone runner endpoint: %w", err)
Expand All @@ -155,9 +172,11 @@ Examples:
if ca, ok := containerApps[app]; ok {
return launchContainerApp(cmd, ca, ep.container, image, port, detach, appArgs, dryRun)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can remove these line space changes which i think is not part of changes

if cli, ok := hostApps[app]; ok {
return launchHostApp(cmd, app, ep.host, cli, model, runner, appArgs, dryRun)
}

return fmt.Errorf("unsupported app %q (supported: %s)", app, strings.Join(supportedApps, ", "))
},
}
Expand All @@ -170,6 +189,31 @@ Examples:
return c
}

func launchSandboxedHostApp(cmd *cobra.Command, sandboxTool, app string, appArgs []string, dryRun bool) error {
if err := validateSandboxTool(sandboxTool); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much better if we can retrieve so everything related to the sandbox happens inside the sandbox.go


err, validatedSbxTool = `validateSandboxTool(sandboxTool)

if err != nil {
 return err
}

Then remove the switch ... and directly

 if dryRun {
			cmd.Printf("sbx %s\n", strings.Join(args, " "))
			return nil
		}

		launchCmd := exec.Command("sbx", args...)
		launchCmd.Stdin = os.Stdin
		launchCmd.Stdout = os.Stdout
		launchCmd.Stderr = os.Stderr

		return launchCmd.Run()

return err
}

args := append([]string{app}, appArgs...)

switch sandboxTool {
case "sbx":
if dryRun {
cmd.Printf("sbx %s\n", strings.Join(args, " "))
return nil
}

launchCmd := exec.Command("sbx", args...)
launchCmd.Stdin = os.Stdin
launchCmd.Stdout = os.Stdout
launchCmd.Stderr = os.Stderr

return launchCmd.Run()
default:
return fmt.Errorf("unsupported sandbox tool %q", sandboxTool)
}
}

// listSupportedApps prints all supported apps with their descriptions and install status.
func listSupportedApps(cmd *cobra.Command) error {
cmd.Println("Supported apps:")
Expand Down
1 change: 1 addition & 0 deletions cmd/cli/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ func NewRootCmd(cli *command.DockerCli) *cobra.Command {
newShowCmd(),
newComposeCmd(),
newLaunchCmd(),
newSandboxConfigCmd(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to do this if we do as a configs sub command

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be newConfigCmd, this will be a generic config system, sandbox is just one of the things

newTagCmd(),
newConfigureCmd(),
newPSCmd(),
Expand Down
126 changes: 126 additions & 0 deletions cmd/cli/commands/sandbox.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package commands

import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/spf13/cobra"
)

var allowedSandboxTools = map[string]struct{}{
"sbx": {},
}

func newSandboxConfigCmd() *cobra.Command {
return &cobra.Command{
Use: "config <key> <value>",
Short: "Set model runner configuration values",
Args: cobra.ExactArgs(2),
RunE: func(_ *cobra.Command, args []string) error {
key := args[0]
value := args[1]

if key != "sandbox.tool" {
return fmt.Errorf("unsupported config key %q", key)
}

if err := validateSandboxTool(value); err != nil {
return err
}

return writeSandboxToolConfig(value)
},
}
}

func validateSandboxTool(tool string) error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can change as above mentioned suggestions

if _, ok := allowedSandboxTools[tool]; !ok {
return fmt.Errorf("unsupported sandbox tool %q", tool)
}

return nil
}

func dmrConfigPath() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("unable to determine config directory: %w", err)
}

return filepath.Join(configDir, "dmr", "config.toml"), nil
}

func writeSandboxToolConfig(tool string) error {
path, err := dmrConfigPath()
if err != nil {
return err
}

if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("unable to create config directory: %w", err)
}

content := fmt.Sprintf("[sandbox]\ntool = %q\n", tool)

if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
return fmt.Errorf("unable to write config: %w", err)
}

return nil
}

func readSandboxToolConfig() (string, error) {
path, err := dmrConfigPath()
if err != nil {
return "", err
}

file, err := os.Open(path)
if os.IsNotExist(err) {
return "", nil
}
if err != nil {
return "", fmt.Errorf("unable to read config: %w", err)
}
defer file.Close()

inSandboxSection := false
scanner := bufio.NewScanner(file)

for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())

if line == "" || strings.HasPrefix(line, "#") {
continue
}

if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
inSandboxSection = line == "[sandbox]"
continue
}

if !inSandboxSection {
continue
}

key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}

if strings.TrimSpace(key) != "tool" {
continue
}

return strings.Trim(strings.TrimSpace(value), `"`), nil
}

if err := scanner.Err(); err != nil {
return "", fmt.Errorf("unable to parse config: %w", err)
}

return "", nil
}
156 changes: 156 additions & 0 deletions cmd/cli/commands/sandbox_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package commands

import (
"os"
"path/filepath"
"testing"
)

func TestWriteAndReadSandboxToolConfig(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())

if err := writeSandboxToolConfig("sbx"); err != nil {
t.Fatalf("writeSandboxToolConfig() error = %v", err)
}

got, err := readSandboxToolConfig()
if err != nil {
t.Fatalf("readSandboxToolConfig() error = %v", err)
}

if got != "sbx" {
t.Fatalf("readSandboxToolConfig() = %q, want %q", got, "sbx")
}

configPath, err := dmrConfigPath()
if err != nil {
t.Fatalf("dmrConfigPath() error = %v", err)
}

content, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("ReadFile(%q) error = %v", configPath, err)
}

want := "[sandbox]\ntool = \"sbx\"\n"
if string(content) != want {
t.Fatalf("config content = %q, want %q", string(content), want)
}
}

func TestReadSandboxToolConfigMissingFile(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())

got, err := readSandboxToolConfig()
if err != nil {
t.Fatalf("readSandboxToolConfig() error = %v", err)
}

if got != "" {
t.Fatalf("readSandboxToolConfig() = %q, want empty string", got)
}
}

func TestValidateSandboxToolAllowsSbx(t *testing.T) {
if err := validateSandboxTool("sbx"); err != nil {
t.Fatalf("validateSandboxTool() error = %v", err)
}
}

func TestValidateSandboxToolRejectsUnsupportedTool(t *testing.T) {
err := validateSandboxTool("firejail")
if err == nil {
t.Fatal("validateSandboxTool() error = nil, want error")
}
}

func TestSandboxConfigCommandRejectsUnsupportedKey(t *testing.T) {
cmd := newSandboxConfigCmd()
cmd.SetArgs([]string{"unsupported.key", "sbx"})

if err := cmd.Execute(); err == nil {
t.Fatal("config command error = nil, want error")
}
}

func TestSandboxConfigCommandRejectsUnsupportedTool(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())

cmd := newSandboxConfigCmd()
cmd.SetArgs([]string{"sandbox.tool", "firejail"})

if err := cmd.Execute(); err == nil {
t.Fatal("config command error = nil, want error")
}
}

func TestSandboxConfigCommandWritesConfig(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())

cmd := newSandboxConfigCmd()
cmd.SetArgs([]string{"sandbox.tool", "sbx"})

if err := cmd.Execute(); err != nil {
t.Fatalf("config command error = %v", err)
}

got, err := readSandboxToolConfig()
if err != nil {
t.Fatalf("readSandboxToolConfig() error = %v", err)
}

if got != "sbx" {
t.Fatalf("readSandboxToolConfig() = %q, want %q", got, "sbx")
}
}

func TestLaunchCommandRequiresConfiguredSandboxTool(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())

cmd := newLaunchCmd()
cmd.SetArgs([]string{"opencode"})

if err := cmd.Execute(); err == nil {
t.Fatal("launch command error = nil, want error")
}
}

func TestLaunchCommandUsesConfiguredSandboxTool(t *testing.T) {
configDir := t.TempDir()
binDir := t.TempDir()
outputPath := filepath.Join(t.TempDir(), "output.txt")

t.Setenv("XDG_CONFIG_HOME", configDir)
t.Setenv("TEST_OUTPUT", outputPath)

sbxPath := filepath.Join(binDir, "sbx")
sbxScript := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$TEST_OUTPUT\"\n"

if err := os.WriteFile(sbxPath, []byte(sbxScript), 0o755); err != nil {
t.Fatalf("WriteFile(%q) error = %v", sbxPath, err)
}

oldPath := os.Getenv("PATH")
t.Setenv("PATH", binDir+string(os.PathListSeparator)+oldPath)

if err := writeSandboxToolConfig("sbx"); err != nil {
t.Fatalf("writeSandboxToolConfig() error = %v", err)
}

cmd := newLaunchCmd()
cmd.SetArgs([]string{"opencode", "--", "--help"})

if err := cmd.Execute(); err != nil {
t.Fatalf("launch command error = %v", err)
}

content, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("ReadFile(%q) error = %v", outputPath, err)
}

want := "opencode\n--help\n"
if string(content) != want {
t.Fatalf("sandbox output = %q, want %q", string(content), want)
}
}
2 changes: 2 additions & 0 deletions cmd/cli/docs/reference/docker_model.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pname: docker
plink: docker.yaml
cname:
- docker model bench
- docker model config
- docker model context
- docker model df
- docker model gateway
Expand Down Expand Up @@ -37,6 +38,7 @@ cname:
- docker model version
clink:
- docker_model_bench.yaml
- docker_model_config.yaml
- docker_model_context.yaml
- docker_model_df.yaml
- docker_model_gateway.yaml
Expand Down
Loading
Loading