Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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",
Comment thread
CoderHariswar marked this conversation as resolved.
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)
}

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.

remove extra change

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 {
Comment thread
CoderHariswar marked this conversation as resolved.
Outdated
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(),
Comment thread
CoderHariswar marked this conversation as resolved.
Outdated
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 {
Comment thread
CoderHariswar marked this conversation as resolved.
Outdated
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