package protocol import "strings" const maxRunLogicalFileKeyLength = 160 func ValidateRunJobAssignment(assignment RunJobAssignment) error { if assignment.JobID == "" || assignment.RunEndpointID == "" || assignment.Capability == "" { return ValidationError("jobId, runEndpointId, and capability are required") } switch assignment.Capability { case RunCapabilityConfigWrite, RunCapabilityFilesRead, RunCapabilityFilesWrite: if assignment.ServerInstanceID == "" { return ValidationError("serverInstanceId is required for scoped file jobs") } if !ValidLogicalFileKey(assignment.TargetKey) { return ValidationError("targetKey is not allowed") } } switch assignment.Capability { case RunCapabilityConfigWrite, RunCapabilityFilesWrite: if !ValidScopedInputRef(assignment.InputRef) { return ValidationError("inputRef is not allowed") } } return nil } type ValidationError string func (err ValidationError) Error() string { return string(err) } func ValidLogicalFileKey(key string) bool { trimmed := strings.TrimSpace(key) if trimmed == "" || trimmed != key || len([]rune(key)) > maxRunLogicalFileKeyLength { return false } lower := strings.ToLower(key) if strings.HasPrefix(key, "/") || strings.Contains(key, "..") || strings.Contains(key, `\`) || strings.Contains(key, "://") || strings.Contains(lower, "/users/") || strings.Contains(lower, "password=") || strings.Contains(lower, "secret=") || strings.Contains(lower, "sk-") || strings.Contains(lower, "bearer ") { return false } for _, char := range key { if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' || char == '/' { continue } return false } return true } func ValidScopedInputRef(ref string) bool { trimmed := strings.TrimSpace(ref) lower := strings.ToLower(ref) if trimmed == "" || trimmed != ref || strings.Contains(lower, "/users/") || strings.Contains(lower, "password=") || strings.Contains(lower, "secret=") || strings.Contains(lower, "sk-") || strings.Contains(lower, "bearer ") { return false } return strings.HasPrefix(ref, "input://") || strings.HasPrefix(ref, "artifact://") }