diff --git a/wlauto/__init__.py b/wlauto/__init__.py
index 4149850a..b73f57f4 100644
--- a/wlauto/__init__.py
+++ b/wlauto/__init__.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 #
 
-from wlauto.core.config.core import settings  # NOQA
+from wlauto.core.configuration import settings  # NOQA
 from wlauto.core.device_manager import DeviceManager, RuntimeParameter, CoreParameter  # NOQA
 from wlauto.core.command import Command  # NOQA
 from wlauto.core.workload import Workload  # NOQA
diff --git a/wlauto/commands/record.py b/wlauto/commands/record.py
index 6a77e5a7..373099f9 100644
--- a/wlauto/commands/record.py
+++ b/wlauto/commands/record.py
@@ -16,7 +16,6 @@
 import os
 import sys
 
-from wlauto import PluginLoader, Command, settings
 from wlauto.common.resources import Executable
 from wlauto.core.resource import NO_ONE
 from wlauto.core.resolver import ResourceResolver
diff --git a/wlauto/commands/show.py b/wlauto/commands/show.py
index a1c9a482..e89085e3 100644
--- a/wlauto/commands/show.py
+++ b/wlauto/commands/show.py
@@ -19,7 +19,7 @@ import subprocess
 from cStringIO import StringIO
 
 from wlauto import Command
-from wlauto.core.config.core import settings
+from wlauto.core.configuration import settings
 from wlauto.core import pluginloader
 from wlauto.utils.doc import (get_summary, get_description, get_type_name, format_column, format_body,
                               format_paragraph, indent, strip_inlined_text)
diff --git a/wlauto/core/config/__init__.py b/wlauto/core/config/__init__.py
deleted file mode 100644
index d4154638..00000000
--- a/wlauto/core/config/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from wlauto.core.config.core import settings, ConfigurationPoint, PluginConfiguration
-from wlauto.core.config.core import merge_config_values, WA_CONFIGURATION
diff --git a/wlauto/core/config/core.py b/wlauto/core/config/core.py
deleted file mode 100644
index f6822dca..00000000
--- a/wlauto/core/config/core.py
+++ /dev/null
@@ -1,650 +0,0 @@
-import os
-import logging
-import shutil
-from glob import glob
-from copy import copy
-from itertools import chain
-
-from wlauto.core import pluginloader
-from wlauto.exceptions import ConfigError
-from wlauto.utils.types import integer, boolean, identifier, list_of_strings
-from wlauto.utils.misc import isiterable, get_article
-from wlauto.utils.serializer import read_pod, yaml
-
-
-class ConfigurationPoint(object):
-    """
-    This defines a gneric configuration point for workload automation. This is
-    used to handle global settings, plugin parameters, etc.
-
-    """
-
-    # Mapping for kind conversion; see docs for convert_types below
-    kind_map = {
-        int: integer,
-        bool: boolean,
-    }
-
-    def __init__(self, name,
-                 kind=None,
-                 mandatory=None,
-                 default=None,
-                 override=False,
-                 allowed_values=None,
-                 description=None,
-                 constraint=None,
-                 merge=False,
-                 aliases=None,
-                 convert_types=True):
-        """
-        Create a new Parameter object.
-
-        :param name: The name of the parameter. This will become an instance
-                     member of the plugin object to which the parameter is
-                     applied, so it must be a valid python  identifier. This
-                     is the only mandatory parameter.
-        :param kind: The type of parameter this is. This must be a callable
-                     that takes an arbitrary object and converts it to the
-                     expected type, or raised ``ValueError`` if such conversion
-                     is not possible. Most Python standard types -- ``str``,
-                     ``int``, ``bool``, etc. -- can be used here. This
-                     defaults to ``str`` if not specified.
-        :param mandatory: If set to ``True``, then a non-``None`` value for
-                          this parameter *must* be provided on plugin
-                          object construction, otherwise ``ConfigError``
-                          will be raised.
-        :param default: The default value for this parameter. If no value
-                        is specified on plugin construction, this value
-                        will be used instead. (Note: if this is specified
-                        and is not ``None``, then ``mandatory`` parameter
-                        will be ignored).
-        :param override: A ``bool`` that specifies whether a parameter of
-                         the same name further up the hierarchy should
-                         be overridden. If this is ``False`` (the
-                         default), an exception will be raised by the
-                         ``AttributeCollection`` instead.
-        :param allowed_values: This should be the complete list of allowed
-                               values for this parameter.  Note: ``None``
-                               value will always be allowed, even if it is
-                               not in this list.  If you want to disallow
-                               ``None``, set ``mandatory`` to ``True``.
-        :param constraint: If specified, this must be a callable that takes
-                           the parameter value as an argument and return a
-                           boolean indicating whether the constraint has been
-                           satisfied. Alternatively, can be a two-tuple with
-                           said callable as the first element and a string
-                           describing the constraint as the second.
-        :param merge: The default behaviour when setting a value on an object
-                      that already has that attribute is to overrided with
-                      the new value. If this is set to ``True`` then the two
-                      values will be merged instead. The rules by which the
-                      values are merged will be determined by the types of
-                      the existing and new values -- see
-                      ``merge_config_values`` documentation for details.
-        :param aliases: Alternative names for the same configuration point.
-                        These are largely for backwards compatibility.
-        :param convert_types: If ``True`` (the default), will automatically
-                              convert ``kind`` values from native Python
-                              types to WA equivalents. This allows more
-                              ituitive interprestation of parameter values,
-                              e.g. the string ``"false"`` being interpreted
-                              as ``False`` when specifed as the value for
-                              a boolean Parameter.
-
-        """
-        self.name = identifier(name)
-        if kind is not None and not callable(kind):
-            raise ValueError('Kind must be callable.')
-        if convert_types and kind in self.kind_map:
-            kind = self.kind_map[kind]
-        self.kind = kind
-        self.mandatory = mandatory
-        self.default = default
-        self.override = override
-        self.allowed_values = allowed_values
-        self.description = description
-        if self.kind is None and not self.override:
-            self.kind = str
-        if constraint is not None and not callable(constraint) and not isinstance(constraint, tuple):
-            raise ValueError('Constraint must be callable or a (callable, str) tuple.')
-        self.constraint = constraint
-        self.merge = merge
-        self.aliases = aliases or []
-
-    def match(self, name):
-        if name == self.name:
-            return True
-        elif name in self.aliases:
-            return True
-        return False
-
-    def set_value(self, obj, value=None):
-        if value is None:
-            if self.default is not None:
-                value = self.default
-            elif self.mandatory:
-                msg = 'No values specified for mandatory parameter {} in {}'
-                raise ConfigError(msg.format(self.name, obj.name))
-        else:
-            try:
-                value = self.kind(value)
-            except (ValueError, TypeError):
-                typename = self.get_type_name()
-                msg = 'Bad value "{}" for {}; must be {} {}'
-                article = get_article(typename)
-                raise ConfigError(msg.format(value, self.name, article, typename))
-        if self.merge and hasattr(obj, self.name):
-            value = merge_config_values(getattr(obj, self.name), value)
-        setattr(obj, self.name, value)
-
-    def validate(self, obj):
-        value = getattr(obj, self.name, None)
-
-        if value is not None:
-            if self.allowed_values:
-                self._validate_allowed_values(obj, value)
-            if self.constraint:
-                self._validate_constraint(obj, value)
-        else:
-            if self.mandatory:
-                msg = 'No value specified for mandatory parameter {} in {}.'
-                raise ConfigError(msg.format(self.name, obj.name))
-
-    def get_type_name(self):
-        typename = str(self.kind)
-        if '\'' in typename:
-            typename = typename.split('\'')[1]
-        elif typename.startswith('<function'):
-            typename = typename.split()[1]
-        return typename
-
-    def _validate_allowed_values(self, obj, value):
-        if 'list' in str(self.kind):
-            for v in value:
-                if v not in self.allowed_values:
-                    msg = 'Invalid value {} for {} in {}; must be in {}'
-                    raise ConfigError(msg.format(v, self.name, obj.name, self.allowed_values))
-        else:
-            if value not in self.allowed_values:
-                msg = 'Invalid value {} for {} in {}; must be in {}'
-                raise ConfigError(msg.format(value, self.name, obj.name, self.allowed_values))
-
-    def _validate_constraint(self, obj, value):
-        msg_vals = {'value': value, 'param': self.name, 'plugin': obj.name}
-        if isinstance(self.constraint, tuple) and len(self.constraint) == 2:
-            constraint, msg = self.constraint  # pylint: disable=unpacking-non-sequence
-        elif callable(self.constraint):
-            constraint = self.constraint
-            msg = '"{value}" failed constraint validation for {param} in {plugin}.'
-        else:
-            raise ValueError('Invalid constraint for {}: must be callable or a 2-tuple'.format(self.name))
-        if not constraint(value):
-            raise ConfigError(value, msg.format(**msg_vals))
-
-    def __repr__(self):
-        d = copy(self.__dict__)
-        del d['description']
-        return 'ConfPoint({})'.format(d)
-
-    __str__ = __repr__
-
-
-class ConfigurationPointCollection(object):
-
-    def __init__(self):
-        self._configs = []
-        self._config_map = {}
-
-    def get(self, name, default=None):
-        return self._config_map.get(name, default)
-
-    def add(self, point):
-        if not isinstance(point, ConfigurationPoint):
-            raise ValueError('Mustbe a ConfigurationPoint, got {}'.format(point.__class__))
-        existing = self.get(point.name)
-        if existing:
-            if point.override:
-                new_point = copy(existing)
-                for a, v in point.__dict__.iteritems():
-                    if v is not None:
-                        setattr(new_point, a, v)
-                self.remove(existing)
-                point = new_point
-            else:
-                raise ValueError('Duplicate ConfigurationPoint "{}"'.format(point.name))
-        self._add(point)
-
-    def remove(self, point):
-        self._configs.remove(point)
-        del self._config_map[point.name]
-        for alias in point.aliases:
-            del self._config_map[alias]
-
-    append = add
-
-    def _add(self, point):
-        self._configs.append(point)
-        self._config_map[point.name] = point
-        for alias in point.aliases:
-            if alias in self._config_map:
-                message = 'Clashing alias "{}" between "{}" and "{}"'
-                raise ValueError(message.format(alias, point.name,
-                                                self._config_map[alias].name))
-
-    def __str__(self):
-        str(self._configs)
-
-    __repr__ = __str__
-
-    def __iadd__(self, other):
-        for p in other:
-            self.add(p)
-        return self
-
-    def __iter__(self):
-        return iter(self._configs)
-
-    def __contains__(self, p):
-        if isinstance(p, basestring):
-            return p in self._config_map
-        return p.name in self._config_map
-
-    def __getitem__(self, i):
-        if isinstance(i, int):
-            return self._configs[i]
-        return self._config_map[i]
-
-    def __len__(self):
-        return len(self._configs)
-
-
-class LoggingConfig(dict):
-
-    defaults = {
-        'file_format': '%(asctime)s %(levelname)-8s %(name)s: %(message)s',
-        'verbose_format': '%(asctime)s %(levelname)-8s %(name)s: %(message)s',
-        'regular_format': '%(levelname)-8s %(message)s',
-        'color': True,
-    }
-
-    def __init__(self, config=None):
-        dict.__init__(self)
-        if isinstance(config, dict):
-            config = {identifier(k.lower()): v for k, v in config.iteritems()}
-            self['regular_format'] = config.pop('regular_format', self.defaults['regular_format'])
-            self['verbose_format'] = config.pop('verbose_format', self.defaults['verbose_format'])
-            self['file_format'] = config.pop('file_format', self.defaults['file_format'])
-            self['color'] = config.pop('colour_enabled', self.defaults['color'])  # legacy
-            self['color'] = config.pop('color', self.defaults['color'])
-            if config:
-                message = 'Unexpected logging configuation parameters: {}'
-                raise ValueError(message.format(bad_vals=', '.join(config.keys())))
-        elif config is None:
-            for k, v in self.defaults.iteritems():
-                self[k] = v
-        else:
-            raise ValueError(config)
-
-
-__WA_CONFIGURATION = [
-    ConfigurationPoint(
-        'user_directory',
-        description="""
-        Path to the user directory. This is the location WA will look for
-        user configuration, additional plugins and plugin dependencies.
-        """,
-        kind=str,
-        default=os.path.join(os.path.expanduser('~'), '.workload_automation'),
-    ),
-    ConfigurationPoint(
-        'plugin_packages',
-        kind=list_of_strings,
-        default=[
-            'wlauto.commands',
-            'wlauto.workloads',
-            'wlauto.instrumentation',
-            'wlauto.result_processors',
-            'wlauto.managers',
-            'wlauto.resource_getters',
-        ],
-        description="""
-        List of packages that will be scanned for WA plugins.
-        """,
-    ),
-    ConfigurationPoint(
-        'plugin_paths',
-        kind=list_of_strings,
-        default=[
-            'workloads',
-            'instruments',
-            'targets',
-            'processors',
-
-            # Legacy
-            'managers',
-            'result_processors',
-        ],
-        description="""
-        List of paths that will be scanned for WA plugins.
-        """,
-    ),
-    ConfigurationPoint(
-        'plugin_ignore_paths',
-        kind=list_of_strings,
-        default=[],
-        description="""
-        List of (sub)paths that will be ignored when scanning
-        ``plugin_paths`` for WA plugins.
-        """,
-    ),
-    ConfigurationPoint(
-        'filer_mount_point',
-        description="""
-        The local mount point for the filer hosting WA assets.
-        """,
-    ),
-    ConfigurationPoint(
-        'logging',
-        kind=LoggingConfig,
-        default=LoggingConfig.defaults,
-        description="""
-        WA logging configuration. This should be a dict with a subset
-        of the following keys::
-
-        :normal_format: Logging format used for console output
-        :verbose_format: Logging format used for verbose console output
-        :file_format: Logging format used for run.log
-        :color: If ``True`` (the default), console logging output will
-                contain bash color escape codes. Set this to ``False`` if
-                console output will be piped somewhere that does not know
-                how to handle those.
-        """,
-    ),
-    ConfigurationPoint(
-        'verbosity',
-        kind=int,
-        default=0,
-        description="""
-        Verbosity of console output.
-        """,
-    ),
-    ConfigurationPoint(
-        'default_output_directory',
-        default="wa_output",
-        description="""
-        The default output directory that will be created if not
-        specified when invoking a run.
-        """,
-    ),
-]
-
-WA_CONFIGURATION = {cp.name: cp for cp in __WA_CONFIGURATION}
-
-ENVIRONMENT_VARIABLES = {
-    'WA_USER_DIRECTORY': WA_CONFIGURATION['user_directory'],
-    'WA_PLUGIN_PATHS': WA_CONFIGURATION['plugin_paths'],
-    'WA_EXTENSION_PATHS': WA_CONFIGURATION['plugin_paths'],  # plugin_paths (legacy)
-}
-
-
-class WAConfiguration(object):
-    """
-    This is configuration for Workload Automation framework as a whole. This
-    does not track configuration for WA runs. Rather, this tracks "meta"
-    configuration, such as various locations WA looks for things, logging
-    configuration etc.
-
-    """
-
-    basename = 'config'
-
-    @property
-    def dependencies_directory(self):
-        return os.path.join(self.user_directory, 'dependencies')
-
-    def __init__(self):
-        self.user_directory = ''
-        self.plugin_packages = []
-        self.plugin_paths = []
-        self.plugin_ignore_paths = []
-        self.config_paths = []
-        self.logging = {}
-        self._logger = logging.getLogger('settings')
-        for confpoint in WA_CONFIGURATION.itervalues():
-            confpoint.set_value(self)
-
-    def load_environment(self):
-        for name, confpoint in ENVIRONMENT_VARIABLES.iteritems():
-            value = os.getenv(name)
-            if value:
-                confpoint.set_value(self, value)
-        self._expand_paths()
-
-    def load_config_file(self, path):
-        self.load(read_pod(path))
-        if path not in self.config_paths:
-            self.config_paths.append(config_paths)
-
-    def load_user_config(self):
-        globpath = os.path.join(self.user_directory, '{}.*'.format(self.basename))
-        for path in glob(globpath):
-            ext = os.path.splitext(path)[1].lower()
-            if ext in ['.pyc', '.pyo']:
-                continue
-            self.load_config_file(path)
-
-    def load(self, config):
-        for name, value in config.iteritems():
-            if name in WA_CONFIGURATION:
-                confpoint = WA_CONFIGURATION[name]
-                confpoint.set_value(self, value)
-        self._expand_paths()
-
-    def set(self, name, value):
-        if name not in WA_CONFIGURATION:
-            raise ConfigError('Unknown WA configuration "{}"'.format(name))
-        WA_CONFIGURATION[name].set_value(self, value)
-
-    def initialize_user_directory(self, overwrite=False):
-        """
-        Initialize a fresh user environment creating the workload automation.
-
-        """
-        if os.path.exists(self.user_directory):
-            if not overwrite:
-                raise ConfigError('Environment {} already exists.'.format(self.user_directory))
-            shutil.rmtree(self.user_directory)
-
-        self._expand_paths()
-        os.makedirs(self.dependencies_directory)
-        for path in self.plugin_paths:
-            os.makedirs(path)
-
-        with open(os.path.join(self.user_directory, 'config.yaml'), 'w') as _:
-            yaml.dump(self.to_pod())
-
-        if os.getenv('USER') == 'root':
-            # If running with sudo on POSIX, change the ownership to the real user.
-            real_user = os.getenv('SUDO_USER')
-            if real_user:
-                import pwd  # done here as module won't import on win32
-                user_entry = pwd.getpwnam(real_user)
-                uid, gid = user_entry.pw_uid, user_entry.pw_gid
-                os.chown(self.user_directory, uid, gid)
-                # why, oh why isn't there a recusive=True option for os.chown?
-                for root, dirs, files in os.walk(self.user_directory):
-                    for d in dirs:
-                        os.chown(os.path.join(root, d), uid, gid)
-                    for f in files:
-                        os.chown(os.path.join(root, f), uid, gid)
-
-    @staticmethod
-    def from_pod(pod):
-        instance = WAConfiguration()
-        instance.load(pod)
-        return instance
-
-    def to_pod(self):
-        return dict(
-            user_directory=self.user_directory,
-            plugin_packages=self.plugin_packages,
-            plugin_paths=self.plugin_paths,
-            plugin_ignore_paths=self.plugin_ignore_paths,
-            logging=self.logging,
-        )
-
-    def _expand_paths(self):
-        self.dependencies_directory = os.path.join(self.user_directory,
-                                                   self.dependencies_directory)
-        expanded = []
-        for path in self.plugin_paths:
-            path = os.path.expanduser(path)
-            path = os.path.expandvars(path)
-            expanded.append(os.path.join(self.user_directory, path))
-        self.plugin_paths = expanded
-        expanded = []
-        for path in self.plugin_ignore_paths:
-            path = os.path.expanduser(path)
-            path = os.path.expandvars(path)
-            expanded.append(os.path.join(self.user_directory, path))
-        self.plugin_ignore_paths = expanded
-
-
-class PluginConfiguration(object):
-    """ Maintains a mapping of plugin_name --> plugin_config. """
-
-    def __init__(self, loader=pluginloader):
-        self.loader = loader
-        self.config = {}
-
-    def update(self, name, config):
-        if not hasattr(config, 'get'):
-            raise ValueError('config must be a dict-like object got: {}'.format(config))
-        name, alias_config = self.loader.resolve_alias(name)
-        existing_config = self.config.get(name)
-        if existing_config is None:
-            existing_config = alias_config
-
-        new_config = config or {}
-        self.config[name] = merge_config_values(existing_config, new_config)
-
-
-def merge_config_values(base, other):
-    """
-    This is used to merge two objects, typically when setting the value of a
-    ``ConfigurationPoint``. First, both objects are categorized into
-
-        c: A scalar value. Basically, most objects. These values
-           are treated as atomic, and not mergeable.
-        s: A sequence. Anything iterable that is not a dict or
-           a string (strings are considered scalars).
-        m: A key-value mapping. ``dict`` and it's derivatives.
-        n: ``None``.
-        o: A mergeable object; this is an object that implements both
-          ``merge_with`` and ``merge_into`` methods.
-
-    The merge rules based on the two categories are then as follows:
-
-        (c1, c2) --> c2
-        (s1, s2) --> s1 . s2
-        (m1, m2) --> m1 . m2
-        (c, s) --> [c] . s
-        (s, c) --> s . [c]
-        (s, m) --> s . [m]
-        (m, s) --> [m] . s
-        (m, c) --> ERROR
-        (c, m) --> ERROR
-        (o, X) --> o.merge_with(X)
-        (X, o) --> o.merge_into(X)
-        (X, n) --> X
-        (n, X) --> X
-
-    where:
-
-        '.'  means concatenation (for maps, contcationation of (k, v) streams
-             then converted back into a map). If the types of the two objects
-             differ, the type of ``other`` is used for the result.
-        'X'  means "any category"
-        '[]' used to indicate a literal sequence (not necessarily a ``list``).
-             when this is concatenated with an actual sequence, that sequencies
-             type is used.
-
-    notes:
-
-        - When a mapping is combined with a sequence, that mapping is
-          treated as a scalar value.
-        - When combining two mergeable objects, they're combined using
-          ``o1.merge_with(o2)`` (_not_ using o2.merge_into(o1)).
-        - Combining anything with ``None`` yields that value, irrespective
-          of the order. So a ``None`` value is eqivalent to the corresponding
-          item being omitted.
-        - When both values are scalars, merging is equivalent to overwriting.
-        - There is no recursion (e.g. if map values are lists, they will not
-          be merged; ``other`` will overwrite ``base`` values). If complicated
-          merging semantics (such as recursion) are required, they should be
-          implemented within custom mergeable types (i.e. those that implement
-          ``merge_with`` and ``merge_into``).
-
-    While this can be used as a generic "combine any two arbitry objects"
-    function, the semantics have been selected specifically for merging
-    configuration point values.
-
-    """
-    cat_base = categorize(base)
-    cat_other = categorize(other)
-
-    if cat_base == 'n':
-        return other
-    elif cat_other == 'n':
-        return base
-
-    if cat_base == 'o':
-        return base.merge_with(other)
-    elif cat_other == 'o':
-        return other.merge_into(base)
-
-    if cat_base == 'm':
-        if cat_other == 's':
-            return merge_sequencies([base], other)
-        elif cat_other == 'm':
-            return merge_maps(base, other)
-        else:
-            message = 'merge error ({}, {}): "{}" and "{}"'
-            raise ValueError(message.format(cat_base, cat_other, base, other))
-    elif cat_base == 's':
-        if cat_other == 's':
-            return merge_sequencies(base, other)
-        else:
-            return merge_sequencies(base, [other])
-    else:  # cat_base == 'c'
-        if cat_other == 's':
-            return merge_sequencies([base], other)
-        elif cat_other == 'm':
-            message = 'merge error ({}, {}): "{}" and "{}"'
-            raise ValueError(message.format(cat_base, cat_other, base, other))
-        else:
-            return other
-
-
-def merge_sequencies(s1, s2):
-    return type(s2)(chain(s1, s2))
-
-
-def merge_maps(m1, m2):
-    return type(m2)(chain(m1.iteritems(), m2.iteritems()))
-
-
-def categorize(v):
-    if hasattr(v, 'merge_with') and hasattr(v, 'merge_into'):
-        return 'o'
-    elif hasattr(v, 'iteritems'):
-        return 'm'
-    elif isiterable(v):
-        return 's'
-    elif v is None:
-        return 'n'
-    else:
-        return 'c'
-
-
-settings = WAConfiguration()
diff --git a/wlauto/core/configuration.py b/wlauto/core/configuration.py
index 082fe110..e450088b 100644
--- a/wlauto/core/configuration.py
+++ b/wlauto/core/configuration.py
@@ -15,17 +15,657 @@
 
 
 import os
-import json
 from copy import copy
 from collections import OrderedDict
+import logging
+import shutil
+from glob import glob
+from itertools import chain
 
 from wlauto.exceptions import ConfigError
 from wlauto.utils.misc import merge_dicts, merge_lists, load_struct_from_file
-from wlauto.utils.types import regex_type, identifier
-from wlauto.core.config.core import settings
+from wlauto.utils.types import regex_type, identifier, integer, boolean, list_of_strings
 from wlauto.core import pluginloader
+from wlauto.utils.serializer import json, WAJSONEncoder
+from wlauto.exceptions import ConfigError
+from wlauto.utils.misc import isiterable, get_article
+from wlauto.utils.serializer import read_pod, yaml
 
 
+class ConfigurationPoint(object):
+    """
+    This defines a gneric configuration point for workload automation. This is
+    used to handle global settings, plugin parameters, etc.
+
+    """
+
+    # Mapping for kind conversion; see docs for convert_types below
+    kind_map = {
+        int: integer,
+        bool: boolean,
+    }
+
+    def __init__(self, name,
+                 kind=None,
+                 mandatory=None,
+                 default=None,
+                 override=False,
+                 allowed_values=None,
+                 description=None,
+                 constraint=None,
+                 merge=False,
+                 aliases=None,
+                 convert_types=True):
+        """
+        Create a new Parameter object.
+
+        :param name: The name of the parameter. This will become an instance
+                     member of the plugin object to which the parameter is
+                     applied, so it must be a valid python  identifier. This
+                     is the only mandatory parameter.
+        :param kind: The type of parameter this is. This must be a callable
+                     that takes an arbitrary object and converts it to the
+                     expected type, or raised ``ValueError`` if such conversion
+                     is not possible. Most Python standard types -- ``str``,
+                     ``int``, ``bool``, etc. -- can be used here. This
+                     defaults to ``str`` if not specified.
+        :param mandatory: If set to ``True``, then a non-``None`` value for
+                          this parameter *must* be provided on plugin
+                          object construction, otherwise ``ConfigError``
+                          will be raised.
+        :param default: The default value for this parameter. If no value
+                        is specified on plugin construction, this value
+                        will be used instead. (Note: if this is specified
+                        and is not ``None``, then ``mandatory`` parameter
+                        will be ignored).
+        :param override: A ``bool`` that specifies whether a parameter of
+                         the same name further up the hierarchy should
+                         be overridden. If this is ``False`` (the
+                         default), an exception will be raised by the
+                         ``AttributeCollection`` instead.
+        :param allowed_values: This should be the complete list of allowed
+                               values for this parameter.  Note: ``None``
+                               value will always be allowed, even if it is
+                               not in this list.  If you want to disallow
+                               ``None``, set ``mandatory`` to ``True``.
+        :param constraint: If specified, this must be a callable that takes
+                           the parameter value as an argument and return a
+                           boolean indicating whether the constraint has been
+                           satisfied. Alternatively, can be a two-tuple with
+                           said callable as the first element and a string
+                           describing the constraint as the second.
+        :param merge: The default behaviour when setting a value on an object
+                      that already has that attribute is to overrided with
+                      the new value. If this is set to ``True`` then the two
+                      values will be merged instead. The rules by which the
+                      values are merged will be determined by the types of
+                      the existing and new values -- see
+                      ``merge_config_values`` documentation for details.
+        :param aliases: Alternative names for the same configuration point.
+                        These are largely for backwards compatibility.
+        :param convert_types: If ``True`` (the default), will automatically
+                              convert ``kind`` values from native Python
+                              types to WA equivalents. This allows more
+                              ituitive interprestation of parameter values,
+                              e.g. the string ``"false"`` being interpreted
+                              as ``False`` when specifed as the value for
+                              a boolean Parameter.
+
+        """
+        self.name = identifier(name)
+        if kind is not None and not callable(kind):
+            raise ValueError('Kind must be callable.')
+        if convert_types and kind in self.kind_map:
+            kind = self.kind_map[kind]
+        self.kind = kind
+        self.mandatory = mandatory
+        self.default = default
+        self.override = override
+        self.allowed_values = allowed_values
+        self.description = description
+        if self.kind is None and not self.override:
+            self.kind = str
+        if constraint is not None and not callable(constraint) and not isinstance(constraint, tuple):
+            raise ValueError('Constraint must be callable or a (callable, str) tuple.')
+        self.constraint = constraint
+        self.merge = merge
+        self.aliases = aliases or []
+
+    def match(self, name):
+        if name == self.name:
+            return True
+        elif name in self.aliases:
+            return True
+        return False
+
+    def set_value(self, obj, value=None):
+        if value is None:
+            if self.default is not None:
+                value = self.default
+            elif self.mandatory:
+                msg = 'No values specified for mandatory parameter {} in {}'
+                raise ConfigError(msg.format(self.name, obj.name))
+        else:
+            try:
+                value = self.kind(value)
+            except (ValueError, TypeError):
+                typename = self.get_type_name()
+                msg = 'Bad value "{}" for {}; must be {} {}'
+                article = get_article(typename)
+                raise ConfigError(msg.format(value, self.name, article, typename))
+        if self.merge and hasattr(obj, self.name):
+            value = merge_config_values(getattr(obj, self.name), value)
+        setattr(obj, self.name, value)
+
+    def validate(self, obj):
+        value = getattr(obj, self.name, None)
+
+        if value is not None:
+            if self.allowed_values:
+                self._validate_allowed_values(obj, value)
+            if self.constraint:
+                self._validate_constraint(obj, value)
+        else:
+            if self.mandatory:
+                msg = 'No value specified for mandatory parameter {} in {}.'
+                raise ConfigError(msg.format(self.name, obj.name))
+
+    def get_type_name(self):
+        typename = str(self.kind)
+        if '\'' in typename:
+            typename = typename.split('\'')[1]
+        elif typename.startswith('<function'):
+            typename = typename.split()[1]
+        return typename
+
+    def _validate_allowed_values(self, obj, value):
+        if 'list' in str(self.kind):
+            for v in value:
+                if v not in self.allowed_values:
+                    msg = 'Invalid value {} for {} in {}; must be in {}'
+                    raise ConfigError(msg.format(v, self.name, obj.name, self.allowed_values))
+        else:
+            if value not in self.allowed_values:
+                msg = 'Invalid value {} for {} in {}; must be in {}'
+                raise ConfigError(msg.format(value, self.name, obj.name, self.allowed_values))
+
+    def _validate_constraint(self, obj, value):
+        msg_vals = {'value': value, 'param': self.name, 'plugin': obj.name}
+        if isinstance(self.constraint, tuple) and len(self.constraint) == 2:
+            constraint, msg = self.constraint  # pylint: disable=unpacking-non-sequence
+        elif callable(self.constraint):
+            constraint = self.constraint
+            msg = '"{value}" failed constraint validation for {param} in {plugin}.'
+        else:
+            raise ValueError('Invalid constraint for {}: must be callable or a 2-tuple'.format(self.name))
+        if not constraint(value):
+            raise ConfigError(value, msg.format(**msg_vals))
+
+    def __repr__(self):
+        d = copy(self.__dict__)
+        del d['description']
+        return 'ConfPoint({})'.format(d)
+
+    __str__ = __repr__
+
+
+class ConfigurationPointCollection(object):
+
+    def __init__(self):
+        self._configs = []
+        self._config_map = {}
+
+    def get(self, name, default=None):
+        return self._config_map.get(name, default)
+
+    def add(self, point):
+        if not isinstance(point, ConfigurationPoint):
+            raise ValueError('Mustbe a ConfigurationPoint, got {}'.format(point.__class__))
+        existing = self.get(point.name)
+        if existing:
+            if point.override:
+                new_point = copy(existing)
+                for a, v in point.__dict__.iteritems():
+                    if v is not None:
+                        setattr(new_point, a, v)
+                self.remove(existing)
+                point = new_point
+            else:
+                raise ValueError('Duplicate ConfigurationPoint "{}"'.format(point.name))
+        self._add(point)
+
+    def remove(self, point):
+        self._configs.remove(point)
+        del self._config_map[point.name]
+        for alias in point.aliases:
+            del self._config_map[alias]
+
+    append = add
+
+    def _add(self, point):
+        self._configs.append(point)
+        self._config_map[point.name] = point
+        for alias in point.aliases:
+            if alias in self._config_map:
+                message = 'Clashing alias "{}" between "{}" and "{}"'
+                raise ValueError(message.format(alias, point.name,
+                                                self._config_map[alias].name))
+
+    def __str__(self):
+        str(self._configs)
+
+    __repr__ = __str__
+
+    def __iadd__(self, other):
+        for p in other:
+            self.add(p)
+        return self
+
+    def __iter__(self):
+        return iter(self._configs)
+
+    def __contains__(self, p):
+        if isinstance(p, basestring):
+            return p in self._config_map
+        return p.name in self._config_map
+
+    def __getitem__(self, i):
+        if isinstance(i, int):
+            return self._configs[i]
+        return self._config_map[i]
+
+    def __len__(self):
+        return len(self._configs)
+
+
+class LoggingConfig(dict):
+
+    defaults = {
+        'file_format': '%(asctime)s %(levelname)-8s %(name)s: %(message)s',
+        'verbose_format': '%(asctime)s %(levelname)-8s %(name)s: %(message)s',
+        'regular_format': '%(levelname)-8s %(message)s',
+        'color': True,
+    }
+
+    def __init__(self, config=None):
+        dict.__init__(self)
+        if isinstance(config, dict):
+            config = {identifier(k.lower()): v for k, v in config.iteritems()}
+            self['regular_format'] = config.pop('regular_format', self.defaults['regular_format'])
+            self['verbose_format'] = config.pop('verbose_format', self.defaults['verbose_format'])
+            self['file_format'] = config.pop('file_format', self.defaults['file_format'])
+            self['color'] = config.pop('colour_enabled', self.defaults['color'])  # legacy
+            self['color'] = config.pop('color', self.defaults['color'])
+            if config:
+                message = 'Unexpected logging configuation parameters: {}'
+                raise ValueError(message.format(bad_vals=', '.join(config.keys())))
+        elif config is None:
+            for k, v in self.defaults.iteritems():
+                self[k] = v
+        else:
+            raise ValueError(config)
+
+
+__WA_CONFIGURATION = [
+    ConfigurationPoint(
+        'user_directory',
+        description="""
+        Path to the user directory. This is the location WA will look for
+        user configuration, additional plugins and plugin dependencies.
+        """,
+        kind=str,
+        default=os.path.join(os.path.expanduser('~'), '.workload_automation'),
+    ),
+    ConfigurationPoint(
+        'plugin_packages',
+        kind=list_of_strings,
+        default=[
+            'wlauto.commands',
+            'wlauto.workloads',
+            'wlauto.instrumentation',
+            'wlauto.result_processors',
+            'wlauto.managers',
+            'wlauto.resource_getters',
+        ],
+        description="""
+        List of packages that will be scanned for WA plugins.
+        """,
+    ),
+    ConfigurationPoint(
+        'plugin_paths',
+        kind=list_of_strings,
+        default=[
+            'workloads',
+            'instruments',
+            'targets',
+            'processors',
+
+            # Legacy
+            'managers',
+            'result_processors',
+        ],
+        description="""
+        List of paths that will be scanned for WA plugins.
+        """,
+    ),
+    ConfigurationPoint(
+        'plugin_ignore_paths',
+        kind=list_of_strings,
+        default=[],
+        description="""
+        List of (sub)paths that will be ignored when scanning
+        ``plugin_paths`` for WA plugins.
+        """,
+    ),
+    ConfigurationPoint(
+        'assets_repository',
+        description="""
+        The local mount point for the filer hosting WA assets.
+        """,
+    ),
+    ConfigurationPoint(
+        'logging',
+        kind=LoggingConfig,
+        default=LoggingConfig.defaults,
+        description="""
+        WA logging configuration. This should be a dict with a subset
+        of the following keys::
+
+        :normal_format: Logging format used for console output
+        :verbose_format: Logging format used for verbose console output
+        :file_format: Logging format used for run.log
+        :color: If ``True`` (the default), console logging output will
+                contain bash color escape codes. Set this to ``False`` if
+                console output will be piped somewhere that does not know
+                how to handle those.
+        """,
+    ),
+    ConfigurationPoint(
+        'verbosity',
+        kind=int,
+        default=0,
+        description="""
+        Verbosity of console output.
+        """,
+    ),
+    ConfigurationPoint(
+        'default_output_directory',
+        default="wa_output",
+        description="""
+        The default output directory that will be created if not
+        specified when invoking a run.
+        """,
+    ),
+]
+
+WA_CONFIGURATION = {cp.name: cp for cp in __WA_CONFIGURATION}
+
+ENVIRONMENT_VARIABLES = {
+    'WA_USER_DIRECTORY': WA_CONFIGURATION['user_directory'],
+    'WA_PLUGIN_PATHS': WA_CONFIGURATION['plugin_paths'],
+    'WA_EXTENSION_PATHS': WA_CONFIGURATION['plugin_paths'],  # plugin_paths (legacy)
+}
+
+
+class WAConfiguration(object):
+    """
+    This is configuration for Workload Automation framework as a whole. This
+    does not track configuration for WA runs. Rather, this tracks "meta"
+    configuration, such as various locations WA looks for things, logging
+    configuration etc.
+
+    """
+
+    basename = 'config'
+
+    @property
+    def dependencies_directory(self):
+        return os.path.join(self.user_directory, 'dependencies')
+
+    def __init__(self):
+        self.user_directory = ''
+        self.plugin_packages = []
+        self.plugin_paths = []
+        self.plugin_ignore_paths = []
+        self.config_paths = []
+        self.logging = {}
+        self._logger = logging.getLogger('settings')
+        for confpoint in WA_CONFIGURATION.itervalues():
+            confpoint.set_value(self)
+
+    def load_environment(self):
+        for name, confpoint in ENVIRONMENT_VARIABLES.iteritems():
+            value = os.getenv(name)
+            if value:
+                confpoint.set_value(self, value)
+        self._expand_paths()
+
+    def load_config_file(self, path):
+        self.load(read_pod(path))
+        if path not in self.config_paths:
+            self.config_paths.append(path)
+
+    def load_user_config(self):
+        globpath = os.path.join(self.user_directory, '{}.*'.format(self.basename))
+        for path in glob(globpath):
+            ext = os.path.splitext(path)[1].lower()
+            if ext in ['.pyc', '.pyo', '.py~']:
+                continue
+            self.load_config_file(path)
+
+    def load(self, config):
+        for name, value in config.iteritems():
+            if name in WA_CONFIGURATION:
+                confpoint = WA_CONFIGURATION[name]
+                confpoint.set_value(self, value)
+        self._expand_paths()
+
+    def set(self, name, value):
+        if name not in WA_CONFIGURATION:
+            raise ConfigError('Unknown WA configuration "{}"'.format(name))
+        WA_CONFIGURATION[name].set_value(self, value)
+
+    def initialize_user_directory(self, overwrite=False):
+        """
+        Initialize a fresh user environment creating the workload automation.
+
+        """
+        if os.path.exists(self.user_directory):
+            if not overwrite:
+                raise ConfigError('Environment {} already exists.'.format(self.user_directory))
+            shutil.rmtree(self.user_directory)
+
+        self._expand_paths()
+        os.makedirs(self.dependencies_directory)
+        for path in self.plugin_paths:
+            os.makedirs(path)
+
+        with open(os.path.join(self.user_directory, 'config.yaml'), 'w') as _:
+            yaml.dump(self.to_pod())
+
+        if os.getenv('USER') == 'root':
+            # If running with sudo on POSIX, change the ownership to the real user.
+            real_user = os.getenv('SUDO_USER')
+            if real_user:
+                import pwd  # done here as module won't import on win32
+                user_entry = pwd.getpwnam(real_user)
+                uid, gid = user_entry.pw_uid, user_entry.pw_gid
+                os.chown(self.user_directory, uid, gid)
+                # why, oh why isn't there a recusive=True option for os.chown?
+                for root, dirs, files in os.walk(self.user_directory):
+                    for d in dirs:
+                        os.chown(os.path.join(root, d), uid, gid)
+                    for f in files:
+                        os.chown(os.path.join(root, f), uid, gid)
+
+    @staticmethod
+    def from_pod(pod):
+        instance = WAConfiguration()
+        instance.load(pod)
+        return instance
+
+    def to_pod(self):
+        return dict(
+            user_directory=self.user_directory,
+            plugin_packages=self.plugin_packages,
+            plugin_paths=self.plugin_paths,
+            plugin_ignore_paths=self.plugin_ignore_paths,
+            logging=self.logging,
+        )
+
+    def _expand_paths(self):
+        expanded = []
+        for path in self.plugin_paths:
+            path = os.path.expanduser(path)
+            path = os.path.expandvars(path)
+            expanded.append(os.path.join(self.user_directory, path))
+        self.plugin_paths = expanded
+        expanded = []
+        for path in self.plugin_ignore_paths:
+            path = os.path.expanduser(path)
+            path = os.path.expandvars(path)
+            expanded.append(os.path.join(self.user_directory, path))
+        self.plugin_ignore_paths = expanded
+
+
+class PluginConfiguration(object):
+    """ Maintains a mapping of plugin_name --> plugin_config. """
+
+    def __init__(self, loader=pluginloader):
+        self.loader = loader
+        self.config = {}
+
+    def update(self, name, config):
+        if not hasattr(config, 'get'):
+            raise ValueError('config must be a dict-like object got: {}'.format(config))
+        name, alias_config = self.loader.resolve_alias(name)
+        existing_config = self.config.get(name)
+        if existing_config is None:
+            existing_config = alias_config
+
+        new_config = config or {}
+        self.config[name] = merge_config_values(existing_config, new_config)
+
+
+def merge_config_values(base, other):
+    """
+    This is used to merge two objects, typically when setting the value of a
+    ``ConfigurationPoint``. First, both objects are categorized into
+
+        c: A scalar value. Basically, most objects. These values
+           are treated as atomic, and not mergeable.
+        s: A sequence. Anything iterable that is not a dict or
+           a string (strings are considered scalars).
+        m: A key-value mapping. ``dict`` and it's derivatives.
+        n: ``None``.
+        o: A mergeable object; this is an object that implements both
+          ``merge_with`` and ``merge_into`` methods.
+
+    The merge rules based on the two categories are then as follows:
+
+        (c1, c2) --> c2
+        (s1, s2) --> s1 . s2
+        (m1, m2) --> m1 . m2
+        (c, s) --> [c] . s
+        (s, c) --> s . [c]
+        (s, m) --> s . [m]
+        (m, s) --> [m] . s
+        (m, c) --> ERROR
+        (c, m) --> ERROR
+        (o, X) --> o.merge_with(X)
+        (X, o) --> o.merge_into(X)
+        (X, n) --> X
+        (n, X) --> X
+
+    where:
+
+        '.'  means concatenation (for maps, contcationation of (k, v) streams
+             then converted back into a map). If the types of the two objects
+             differ, the type of ``other`` is used for the result.
+        'X'  means "any category"
+        '[]' used to indicate a literal sequence (not necessarily a ``list``).
+             when this is concatenated with an actual sequence, that sequencies
+             type is used.
+
+    notes:
+
+        - When a mapping is combined with a sequence, that mapping is
+          treated as a scalar value.
+        - When combining two mergeable objects, they're combined using
+          ``o1.merge_with(o2)`` (_not_ using o2.merge_into(o1)).
+        - Combining anything with ``None`` yields that value, irrespective
+          of the order. So a ``None`` value is eqivalent to the corresponding
+          item being omitted.
+        - When both values are scalars, merging is equivalent to overwriting.
+        - There is no recursion (e.g. if map values are lists, they will not
+          be merged; ``other`` will overwrite ``base`` values). If complicated
+          merging semantics (such as recursion) are required, they should be
+          implemented within custom mergeable types (i.e. those that implement
+          ``merge_with`` and ``merge_into``).
+
+    While this can be used as a generic "combine any two arbitry objects"
+    function, the semantics have been selected specifically for merging
+    configuration point values.
+
+    """
+    cat_base = categorize(base)
+    cat_other = categorize(other)
+
+    if cat_base == 'n':
+        return other
+    elif cat_other == 'n':
+        return base
+
+    if cat_base == 'o':
+        return base.merge_with(other)
+    elif cat_other == 'o':
+        return other.merge_into(base)
+
+    if cat_base == 'm':
+        if cat_other == 's':
+            return merge_sequencies([base], other)
+        elif cat_other == 'm':
+            return merge_maps(base, other)
+        else:
+            message = 'merge error ({}, {}): "{}" and "{}"'
+            raise ValueError(message.format(cat_base, cat_other, base, other))
+    elif cat_base == 's':
+        if cat_other == 's':
+            return merge_sequencies(base, other)
+        else:
+            return merge_sequencies(base, [other])
+    else:  # cat_base == 'c'
+        if cat_other == 's':
+            return merge_sequencies([base], other)
+        elif cat_other == 'm':
+            message = 'merge error ({}, {}): "{}" and "{}"'
+            raise ValueError(message.format(cat_base, cat_other, base, other))
+        else:
+            return other
+
+
+def merge_sequencies(s1, s2):
+    return type(s2)(chain(s1, s2))
+
+
+def merge_maps(m1, m2):
+    return type(m2)(chain(m1.iteritems(), m2.iteritems()))
+
+
+def categorize(v):
+    if hasattr(v, 'merge_with') and hasattr(v, 'merge_into'):
+        return 'o'
+    elif hasattr(v, 'iteritems'):
+        return 'm'
+    elif isiterable(v):
+        return 's'
+    elif v is None:
+        return 'n'
+    else:
+        return 'c'
+
+settings = WAConfiguration()
+
 class SharedConfiguration(object):
 
     def __init__(self):
diff --git a/wlauto/core/entry_point.py b/wlauto/core/entry_point.py
index 24e48fd9..d7867050 100644
--- a/wlauto/core/entry_point.py
+++ b/wlauto/core/entry_point.py
@@ -21,7 +21,7 @@ import os
 import subprocess
 import warnings
 
-from wlauto.core.config.core import settings
+from wlauto.core.configuration import settings
 from wlauto.core import pluginloader
 from wlauto.exceptions import WAError
 from wlauto.utils.misc import get_traceback
diff --git a/wlauto/core/execution.py b/wlauto/core/execution.py
index c0a7b5a4..0bc99e14 100644
--- a/wlauto/core/execution.py
+++ b/wlauto/core/execution.py
@@ -49,9 +49,8 @@ from itertools import izip_longest
 
 import wlauto.core.signal as signal
 from wlauto.core import instrumentation
-from wlauto.core.config.core import settings
+from wlauto.core.configuration import settings
 from wlauto.core.plugin import Artifact
-from wlauto.core.configuration import RunConfiguration
 from wlauto.core import pluginloader
 from wlauto.core.resolver import ResourceResolver
 from wlauto.core.result import ResultManager, IterationResult, RunResult
diff --git a/wlauto/core/exttype.py b/wlauto/core/exttype.py
index 19014111..5d7a7617 100644
--- a/wlauto/core/exttype.py
+++ b/wlauto/core/exttype.py
@@ -15,7 +15,7 @@
 
 
 # Separate module to avoid circular dependencies
-from wlauto.core.config.core import settings
+from wlauto.core.configuration import settings
 from wlauto.core.plugin import Plugin
 from wlauto.utils.misc import load_class
 from wlauto.core import pluginloader
diff --git a/wlauto/core/plugin.py b/wlauto/core/plugin.py
index 38483fe4..d37169c8 100644
--- a/wlauto/core/plugin.py
+++ b/wlauto/core/plugin.py
@@ -27,9 +27,9 @@ from copy import copy
 
 from wlauto.exceptions import NotFoundError, LoaderError, ValidationError, ConfigError
 from wlauto.utils.misc import isiterable, ensure_directory_exists as _d, walk_modules, load_class, merge_dicts, get_article
-from wlauto.core.config.core import settings
+from wlauto.core.configuration import settings
 from wlauto.utils.types import identifier, integer, boolean
-from wlauto.core.config.core import ConfigurationPoint, ConfigurationPointCollection
+from wlauto.core.configuration import ConfigurationPoint, ConfigurationPointCollection
 
 MODNAME_TRANS = string.maketrans(':/\\.', '____')
 
diff --git a/wlauto/core/pluginloader.py b/wlauto/core/pluginloader.py
index ead0a5eb..0aa8dd3f 100644
--- a/wlauto/core/pluginloader.py
+++ b/wlauto/core/pluginloader.py
@@ -36,7 +36,7 @@ class __LoaderWrapper(object):
         # These imports cannot be done at top level, because of
         # sys.modules manipulation below
         from wlauto.core.plugin import PluginLoader
-        from wlauto.core.config.core import settings
+        from wlauto.core.configuration import settings
         self._loader = PluginLoader(settings.plugin_packages,
                                     settings.plugin_paths,
                                     settings.plugin_ignore_paths)
diff --git a/wlauto/core/resource.py b/wlauto/core/resource.py
index 8e613b99..24e0ae19 100644
--- a/wlauto/core/resource.py
+++ b/wlauto/core/resource.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 #
 
-from wlauto.core.config.core import settings
+from wlauto.core.configuration import settings
 from wlauto.core.plugin import Plugin
 
 
diff --git a/wlauto/utils/log.py b/wlauto/utils/log.py
index 31ea91f5..a4b5d51d 100644
--- a/wlauto/utils/log.py
+++ b/wlauto/utils/log.py
@@ -21,7 +21,7 @@ import threading
 
 import colorama
 
-from wlauto.core.config.core import settings
+from wlauto.core.configuration import settings
 import wlauto.core.signal as signal