Skip to content

Index

axm_config

axm-config.

Non-sensitive runtime config under ~/.axm (env>file>default)

ConfigError

Bases: RuntimeError

Raised when a required config value cannot be resolved.

Source code in packages/axm-config/src/axm_config/resolver.py
Python
class ConfigError(RuntimeError):
    """Raised when a required config value cannot be resolved."""

ExecutionPolicyOverride

Bases: _PolicyBase

Typed per-ticket-type execution override.

Source code in packages/axm-config/src/axm_config/resolver.py
Python
class ExecutionPolicyOverride(_PolicyBase):
    """Typed per-ticket-type execution override."""

    model_config: ClassVar[ConfigDict] = ConfigDict(
        extra="forbid", frozen=True, strict=True
    )

    backend: str | None = None
    model: str | None = None
    analysis_enabled: bool | None = None

    def __init__(
        self,
        *,
        backend: str | None = None,
        model: str | None = None,
        analysis_enabled: bool | None = None,
        **extra: object,
    ) -> None:
        super().__init__(
            backend=backend,
            model=model,
            analysis_enabled=analysis_enabled,
            **extra,
        )

NamespaceStore

Read/write namespace sections of a single ~/.axm/config.toml.

Each namespace maps to a top-level (or nested, for a dotted namespace) TOML table within one config.toml. Reads of an absent or malformed file/section return {} rather than raising, so a consumer can rely on the store at import time without a pre-existing ~/.axm directory. Writes are atomic, preserve every other section, and leave the file 0600. A namespace node may be both a leaf (its own scalar/array keys) and a prefix (nested child namespaces such as [git.default] under [git]): those child sub-tables are re-attached on every write, so setting a key under the parent never erases them. Legacy ~/.axm/<ns>.toml files are folded in on first write.

Source code in packages/axm-config/src/axm_config/store.py
Python
class NamespaceStore:
    """Read/write namespace sections of a single ``~/.axm/config.toml``.

    Each namespace maps to a top-level (or nested, for a dotted namespace)
    TOML table within one ``config.toml``. Reads of an absent or malformed
    file/section return ``{}`` rather than raising, so a consumer can rely on
    the store at import time without a pre-existing ``~/.axm`` directory.
    Writes are atomic, preserve every other section, and leave the file
    ``0600``. A namespace node may be **both** a leaf (its own scalar/array
    keys) **and** a prefix (nested **child** namespaces such as ``[git.default]``
    under ``[git]``): those child sub-tables are re-attached on every write, so
    setting a key under the parent never erases them. Legacy
    ``~/.axm/<ns>.toml`` files are folded in on first write.
    """

    def _config_path(self) -> Path:
        """Resolve the on-disk path of ``config.toml`` under ``axm_home()``.

        Defence in depth on top of the boundary segment validation: the
        resolved path must sit *inside* the resolved ``~/.axm`` home, and
        ``resolve_safe`` refuses a home that itself resolves inside a git
        checkout (a misconfigured ``HOME`` pointing into a repo). A path that
        would escape the home raises :class:`ValueError`.
        """
        home = _safe_home()
        path = (home / CONFIG_FILENAME).resolve()
        if home not in path.parents:
            msg = f"refusing out-of-home store path {path}: escapes {home}"
            raise ValueError(msg)
        return path

    def _legacy_path(self, ns: str) -> Path:
        """Resolve the legacy per-namespace ``~/.axm/<ns>.toml`` path.

        Same in-home containment guard as :meth:`_config_path`. This is the
        previous storage layout; it is read-through and folded into
        ``config.toml`` on the next write.
        """
        home = _safe_home()
        path = (home / f"{ns}.toml").resolve()
        if home not in path.parents:
            msg = f"refusing out-of-home store path {path}: escapes {home}"
            raise ValueError(msg)
        return path

    def _load_config(self) -> dict[str, object]:
        """Return the full parsed ``config.toml`` mapping, or ``{}``.

        A missing file or a malformed TOML payload both degrade to ``{}`` so
        the call never raises for a consumer.
        """
        path = self._config_path()
        try:
            with path.open("rb") as fh:
                return tomllib.load(fh)
        except FileNotFoundError:
            return {}
        except (tomllib.TOMLDecodeError, OSError):
            return {}

    def _read_legacy(self, ns: str) -> dict[str, object]:
        """Return the contents of the legacy ``~/.axm/<ns>.toml``, or ``{}``."""
        path = self._legacy_path(ns)
        try:
            with path.open("rb") as fh:
                return tomllib.load(fh)
        except FileNotFoundError:
            return {}
        except (tomllib.TOMLDecodeError, OSError):
            return {}

    def read_exact(self, ns: str) -> dict[str, object]:
        """Return exactly one stored section without compatibility overlays."""
        config = self._load_config()
        section = _section(config, ns)
        if section:
            return section
        return self._read_legacy(ns)

    def read(self, ns: str) -> dict[str, object]:
        """Return the section for ``ns``, or ``{}`` if absent/corrupt.

        The ``[ns]`` section of ``config.toml`` is returned as a flat mapping
        of that namespace's *own* keys: a dotted namespace maps to a nested
        table, and any nested sub-table is a **child namespace**, not a key, so
        it is excluded from the result. If the section is absent but a legacy
        ``~/.axm/<ns>.toml`` exists, the legacy contents are returned so the
        value stays visible before the fold. A missing file or a malformed
        TOML payload both degrade to ``{}``. Raises
        :class:`~axm_config.resolver.UnsafeHomeError` (a :class:`ConfigError`)
        only when ``~/.axm`` cannot be used safely (HOME inside a git repo).
        """
        config = self._load_config()
        section = _section(config, ns)
        legacy_execution_prefix = "execution."
        canonical_execution_prefix = "execution.v1."
        if ns.startswith(legacy_execution_prefix) and not ns.startswith(
            canonical_execution_prefix
        ):
            ticket_type = ns.removeprefix(legacy_execution_prefix)
            token = ticket_type.encode("utf-8").hex()
            canonical = _section(
                config,
                f"{canonical_execution_prefix}{token}",
            )
            if canonical == {"tombstone": "v1"}:
                return {}
            if canonical:
                return canonical
        if section:
            return section
        return self._read_legacy(ns)

    def write(self, ns: str, key: str, value: object) -> None:
        """Set ``key`` to ``value`` in ``ns``, preserving every other section.

        Read-modify-write of the *whole* ``config.toml``: the full mapping is
        loaded, the ``ns`` section folded with any legacy file and updated, and
        the result serialised to a same-directory temp file atomically moved
        into place via :func:`os.replace`; the file is chmod ``0600``. The
        legacy ``~/.axm/<ns>.toml`` (if any) is removed after the fold.
        """
        config = self._load_config()
        section = self._fold_legacy(config, ns)
        section[key] = value
        _set_section(config, ns, _with_child_tables(config, ns, section))
        self._commit_config(config)
        self._drop_legacy(ns)

    def delete(self, ns: str, key: str) -> None:
        """Remove ``key`` from ``ns``, rewriting atomically (no-op if absent).

        Read-modify-write mirroring :meth:`write` over the whole
        ``config.toml``: the key is popped from the (legacy-folded) section. If
        the section becomes empty it is dropped; if the file then holds no
        section it is unlinked. A missing key (after folding) is a silent
        no-op, but a pending legacy fold is still applied.
        """
        config = self._load_config()
        section = self._fold_legacy(config, ns)
        had_legacy = self._legacy_path(ns).exists()
        if key not in section and not had_legacy:
            return
        section.pop(key, None)
        if section:
            _set_section(config, ns, section)
        else:
            _drop_section(config, ns)
        if config:
            self._commit_config(config)
        else:
            self._config_path().unlink(missing_ok=True)
        self._drop_legacy(ns)

    def replace_section(self, ns: str, section: dict[str, object]) -> None:
        """Atomically replace a namespace's own leaf while keeping children."""
        config = self._load_config()
        current = _section(config, ns)
        had_legacy = self._legacy_path(ns).exists()
        if (not section and not current and not had_legacy) or (
            section == current and not had_legacy
        ):
            return

        replacement = _with_child_tables(config, ns, dict(section))
        if replacement:
            _set_section(config, ns, replacement)
        else:
            _drop_section(config, ns)

        if config:
            self._commit_config(config)
        else:
            self._config_path().unlink(missing_ok=True)
        self._drop_legacy(ns)

    def namespaces(self) -> list[str]:
        """Return every namespace path present in ``config.toml`` or legacy.

        A leaf table (a mapping whose values are all scalars/arrays, i.e. an
        actual namespace section) yields its dotted path. Legacy
        ``~/.axm/<ns>.toml`` files contribute their stem. Used by the doctor to
        enumerate "all known" namespaces when none is requested.
        """
        found = set(_leaf_paths(self._load_config()))
        home = _safe_home()
        for legacy in home.glob("*.toml"):
            if legacy.name != CONFIG_FILENAME and _is_namespace(legacy.stem):
                found.add(legacy.stem)
        return sorted(found)

    def _fold_legacy(self, config: dict[str, object], ns: str) -> dict[str, object]:
        """Return the ``ns`` section merged with any legacy file (legacy base).

        The fold is bounded to recognised namespaces: when ``ns`` is not a valid
        namespace (:func:`_is_namespace`) no legacy file is read, so an unrelated
        ``~/.axm/<ns>.toml`` owned by another tool is never absorbed.
        """
        section = _section(config, ns)
        if not _is_namespace(ns):
            return section
        legacy = self._read_legacy(ns)
        merged: dict[str, object] = {**legacy, **section}
        return merged

    def _drop_legacy(self, ns: str) -> None:
        """Remove the legacy ``~/.axm/<ns>.toml`` after a successful fold.

        Bounded to recognised namespaces (:func:`_is_namespace`): a foreign
        top-level ``.toml`` whose stem is not a valid namespace is left on disk
        untouched rather than unlinked.
        """
        if not _is_namespace(ns):
            return
        self._legacy_path(ns).unlink(missing_ok=True)

    def _commit_config(self, config: dict[str, object]) -> None:
        """Serialise ``config`` to a same-dir temp file and atomically swap it.

        The mapping is written to a ``NamedTemporaryFile`` under ``~/.axm`` and
        moved onto ``config.toml`` via :func:`os.replace`; the resulting file
        is chmod ``0600``.
        """
        path = self._config_path()
        payload = tomli_w.dumps(config).encode("utf-8")
        with NamedTemporaryFile(mode="wb", dir=path.parent, delete=False) as tmp:
            tmp.write(payload)
            tmp_path = Path(tmp.name)
        self._commit(tmp_path, path)

    def _commit(self, tmp_path: Path, path: Path) -> None:
        """Atomically move ``tmp_path`` onto ``path``, never leaking the temp.

        :func:`os.replace` is the atomic swap; if it (or the follow-up
        ``chmod``) raises, the staged temp file would otherwise linger under
        ``~/.axm``. A ``try/finally`` unlinks it on the error path while a
        successful replace consumes it (the ``missing_ok`` unlink is then a
        no-op on the already-moved path).
        """
        try:
            os.replace(tmp_path, path)
            if os.name == "posix":
                os.chmod(path, NAMESPACE_FILE_MODE)
        finally:
            tmp_path.unlink(missing_ok=True)
delete(ns, key)

Remove key from ns, rewriting atomically (no-op if absent).

Read-modify-write mirroring :meth:write over the whole config.toml: the key is popped from the (legacy-folded) section. If the section becomes empty it is dropped; if the file then holds no section it is unlinked. A missing key (after folding) is a silent no-op, but a pending legacy fold is still applied.

Source code in packages/axm-config/src/axm_config/store.py
Python
def delete(self, ns: str, key: str) -> None:
    """Remove ``key`` from ``ns``, rewriting atomically (no-op if absent).

    Read-modify-write mirroring :meth:`write` over the whole
    ``config.toml``: the key is popped from the (legacy-folded) section. If
    the section becomes empty it is dropped; if the file then holds no
    section it is unlinked. A missing key (after folding) is a silent
    no-op, but a pending legacy fold is still applied.
    """
    config = self._load_config()
    section = self._fold_legacy(config, ns)
    had_legacy = self._legacy_path(ns).exists()
    if key not in section and not had_legacy:
        return
    section.pop(key, None)
    if section:
        _set_section(config, ns, section)
    else:
        _drop_section(config, ns)
    if config:
        self._commit_config(config)
    else:
        self._config_path().unlink(missing_ok=True)
    self._drop_legacy(ns)
namespaces()

Return every namespace path present in config.toml or legacy.

A leaf table (a mapping whose values are all scalars/arrays, i.e. an actual namespace section) yields its dotted path. Legacy ~/.axm/<ns>.toml files contribute their stem. Used by the doctor to enumerate "all known" namespaces when none is requested.

Source code in packages/axm-config/src/axm_config/store.py
Python
def namespaces(self) -> list[str]:
    """Return every namespace path present in ``config.toml`` or legacy.

    A leaf table (a mapping whose values are all scalars/arrays, i.e. an
    actual namespace section) yields its dotted path. Legacy
    ``~/.axm/<ns>.toml`` files contribute their stem. Used by the doctor to
    enumerate "all known" namespaces when none is requested.
    """
    found = set(_leaf_paths(self._load_config()))
    home = _safe_home()
    for legacy in home.glob("*.toml"):
        if legacy.name != CONFIG_FILENAME and _is_namespace(legacy.stem):
            found.add(legacy.stem)
    return sorted(found)
read(ns)

Return the section for ns, or {} if absent/corrupt.

The [ns] section of config.toml is returned as a flat mapping of that namespace's own keys: a dotted namespace maps to a nested table, and any nested sub-table is a child namespace, not a key, so it is excluded from the result. If the section is absent but a legacy ~/.axm/<ns>.toml exists, the legacy contents are returned so the value stays visible before the fold. A missing file or a malformed TOML payload both degrade to {}. Raises :class:~axm_config.resolver.UnsafeHomeError (a :class:ConfigError) only when ~/.axm cannot be used safely (HOME inside a git repo).

Source code in packages/axm-config/src/axm_config/store.py
Python
def read(self, ns: str) -> dict[str, object]:
    """Return the section for ``ns``, or ``{}`` if absent/corrupt.

    The ``[ns]`` section of ``config.toml`` is returned as a flat mapping
    of that namespace's *own* keys: a dotted namespace maps to a nested
    table, and any nested sub-table is a **child namespace**, not a key, so
    it is excluded from the result. If the section is absent but a legacy
    ``~/.axm/<ns>.toml`` exists, the legacy contents are returned so the
    value stays visible before the fold. A missing file or a malformed
    TOML payload both degrade to ``{}``. Raises
    :class:`~axm_config.resolver.UnsafeHomeError` (a :class:`ConfigError`)
    only when ``~/.axm`` cannot be used safely (HOME inside a git repo).
    """
    config = self._load_config()
    section = _section(config, ns)
    legacy_execution_prefix = "execution."
    canonical_execution_prefix = "execution.v1."
    if ns.startswith(legacy_execution_prefix) and not ns.startswith(
        canonical_execution_prefix
    ):
        ticket_type = ns.removeprefix(legacy_execution_prefix)
        token = ticket_type.encode("utf-8").hex()
        canonical = _section(
            config,
            f"{canonical_execution_prefix}{token}",
        )
        if canonical == {"tombstone": "v1"}:
            return {}
        if canonical:
            return canonical
    if section:
        return section
    return self._read_legacy(ns)
read_exact(ns)

Return exactly one stored section without compatibility overlays.

Source code in packages/axm-config/src/axm_config/store.py
Python
def read_exact(self, ns: str) -> dict[str, object]:
    """Return exactly one stored section without compatibility overlays."""
    config = self._load_config()
    section = _section(config, ns)
    if section:
        return section
    return self._read_legacy(ns)
replace_section(ns, section)

Atomically replace a namespace's own leaf while keeping children.

Source code in packages/axm-config/src/axm_config/store.py
Python
def replace_section(self, ns: str, section: dict[str, object]) -> None:
    """Atomically replace a namespace's own leaf while keeping children."""
    config = self._load_config()
    current = _section(config, ns)
    had_legacy = self._legacy_path(ns).exists()
    if (not section and not current and not had_legacy) or (
        section == current and not had_legacy
    ):
        return

    replacement = _with_child_tables(config, ns, dict(section))
    if replacement:
        _set_section(config, ns, replacement)
    else:
        _drop_section(config, ns)

    if config:
        self._commit_config(config)
    else:
        self._config_path().unlink(missing_ok=True)
    self._drop_legacy(ns)
write(ns, key, value)

Set key to value in ns, preserving every other section.

Read-modify-write of the whole config.toml: the full mapping is loaded, the ns section folded with any legacy file and updated, and the result serialised to a same-directory temp file atomically moved into place via :func:os.replace; the file is chmod 0600. The legacy ~/.axm/<ns>.toml (if any) is removed after the fold.

Source code in packages/axm-config/src/axm_config/store.py
Python
def write(self, ns: str, key: str, value: object) -> None:
    """Set ``key`` to ``value`` in ``ns``, preserving every other section.

    Read-modify-write of the *whole* ``config.toml``: the full mapping is
    loaded, the ``ns`` section folded with any legacy file and updated, and
    the result serialised to a same-directory temp file atomically moved
    into place via :func:`os.replace`; the file is chmod ``0600``. The
    legacy ``~/.axm/<ns>.toml`` (if any) is removed after the fold.
    """
    config = self._load_config()
    section = self._fold_legacy(config, ns)
    section[key] = value
    _set_section(config, ns, _with_child_tables(config, ns, section))
    self._commit_config(config)
    self._drop_legacy(ns)

UnsafeHomeError

Bases: ConfigError

Raised when ~/.axm cannot be used safely (e.g. a HOME in a git repo).

A :class:ConfigError subclass so every consumer surface that already catches :class:ConfigError (the CLI, :func:load) degrades cleanly instead of leaking the raw ValueError from :func:axm_config.home.resolve_safe. The security refusal itself is intentional; only its type is narrowed here so callers can handle it.

Source code in packages/axm-config/src/axm_config/resolver.py
Python
class UnsafeHomeError(ConfigError):
    """Raised when ``~/.axm`` cannot be used safely (e.g. a HOME in a git repo).

    A :class:`ConfigError` subclass so every consumer surface that already
    catches :class:`ConfigError` (the CLI, :func:`load`) degrades cleanly
    instead of leaking the raw ``ValueError`` from
    :func:`axm_config.home.resolve_safe`. The security refusal itself is
    intentional; only its *type* is narrowed here so callers can handle it.
    """

axm_home()

Return the resolved ~/.axm directory, creating it 0700 if absent.

Idempotent: a pre-existing directory with looser permissions is tightened back to 0700. Permission calls degrade gracefully on non-POSIX systems.

Source code in packages/axm-config/src/axm_config/home.py
Python
def axm_home() -> Path:
    """Return the resolved ``~/.axm`` directory, creating it ``0700`` if absent.

    Idempotent: a pre-existing directory with looser permissions is tightened
    back to ``0700``. Permission calls degrade gracefully on non-POSIX systems.
    """
    home = (Path.home() / ".axm").resolve()
    home.mkdir(mode=AXM_DIR_MODE, parents=True, exist_ok=True)
    if os.name == "posix":
        os.chmod(home, AXM_DIR_MODE)
    return home

delete(namespace, key)

Remove key from the [namespace] section of config.toml (no-op if absent).

namespace and key are validated first. Deleting an absent key (or a namespace with no file) is a silent no-op — it never raises. After removal the key resolves through the lower layers again (env, then default).

Source code in packages/axm-config/src/axm_config/resolver.py
Python
def delete(namespace: str, key: str) -> None:
    """Remove ``key`` from the ``[namespace]`` section of config.toml (no-op if absent).

    ``namespace`` and ``key`` are validated first. Deleting an absent key (or a
    namespace with no file) is a silent no-op — it never raises. After removal
    the key resolves through the lower layers again (env, then ``default``).
    """
    validate_segment(namespace, kind="namespace")
    validate_segment(key, kind="key")
    _store.delete(namespace, key)

delete_execution_policy(ticket_type)

Idempotently delete one policy leaf while preserving descendants.

Source code in packages/axm-config/src/axm_config/resolver.py
Python
def delete_execution_policy(ticket_type: str) -> None:
    """Idempotently delete one policy leaf while preserving descendants."""
    namespace = _execution_namespace(ticket_type)
    _store.replace_section(namespace, {"tombstone": "v1"})

get(namespace, key, *, default=None)

Return the resolved value for key in namespace.

Precedence is env > file > default. An env value is returned as the raw str from the environment; file values keep their TOML-parsed type.

Source code in packages/axm-config/src/axm_config/resolver.py
Python
def get(namespace: str, key: str, *, default: object = None) -> object:
    """Return the resolved value for ``key`` in ``namespace``.

    Precedence is ``env > file > default``. An env value is returned as the
    raw ``str`` from the environment; file values keep their TOML-parsed type.
    """
    return resolve(namespace, key, default)

get_bool(key, default, *, namespace=PATHS_NAMESPACE)

Resolve a configured boolean while preserving an untouched default.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def get_bool(
    key: str,
    default: bool,
    *,
    namespace: str = PATHS_NAMESPACE,
) -> bool:
    """Resolve a configured boolean while preserving an untouched default."""
    configured = _resolve_configured(namespace, key)
    if configured is _MISSING:
        return default
    if isinstance(configured, bool):
        return configured
    if isinstance(configured, str):
        normalised = configured.lower()
        if normalised in {"1", "true", "yes", "on"}:
            return True
        if normalised in {"0", "false", "no", "off"}:
            return False
    msg = f"invalid boolean for {namespace}.{key}: {configured!r}"
    raise ConfigError(msg)

get_execution_policy(ticket_type)

Resolve one targeted policy, raising ConfigError for malformed values.

Source code in packages/axm-config/src/axm_config/resolver.py
Python
def get_execution_policy(ticket_type: str) -> ExecutionPolicyOverride:
    """Resolve one targeted policy, raising ConfigError for malformed values."""
    namespace = _execution_namespace(ticket_type)
    canonical_values = _store.read(namespace)
    env_backend = os.environ.get(_env_name(namespace, "backend"), _MISSING)
    env_model = os.environ.get(_env_name(namespace, "model"), _MISSING)
    env_analysis = os.environ.get(
        _env_name(namespace, "analysis_enabled"),
        _MISSING,
    )
    env_pair_present = env_backend is not _MISSING or env_model is not _MISSING

    if env_pair_present:
        backend, model = _policy_pair(
            env_backend,
            env_model,
            layer="environment",
        )
    else:
        backend, model = None, None
    if env_analysis is not _MISSING:
        analysis_enabled = _policy_boolean(env_analysis, layer="environment")
    else:
        analysis_enabled = None

    if env_pair_present and env_analysis is not _MISSING:
        return ExecutionPolicyOverride(
            backend=backend,
            model=model,
            analysis_enabled=analysis_enabled,
        )

    file_values = _policy_file_values(ticket_type, canonical_values)
    if not env_pair_present:
        file_policy = _policy_from_values(file_values, layer="file")
        backend, model = file_policy.backend, file_policy.model
    if env_analysis is _MISSING:
        analysis_enabled = _policy_boolean(
            file_values.get("analysis_enabled", _MISSING),
            layer="file",
        )
    return ExecutionPolicyOverride(
        backend=backend,
        model=model,
        analysis_enabled=analysis_enabled,
    )

get_int(key, default, *, namespace=PATHS_NAMESPACE)

Resolve a configured integer while preserving an untouched default.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def get_int(
    key: str,
    default: int,
    *,
    namespace: str = PATHS_NAMESPACE,
) -> int:
    """Resolve a configured integer while preserving an untouched default."""
    configured = _resolve_configured(namespace, key)
    if configured is _MISSING:
        return default
    if type(configured) is int:
        return configured
    if isinstance(configured, str):
        try:
            return int(configured)
        except ValueError as exc:
            msg = f"invalid integer for {namespace}.{key}: {configured!r}"
            raise ConfigError(msg) from exc
    msg = (
        f"invalid integer for {namespace}.{key}: "
        f"expected an integer, got {type(configured).__name__}"
    )
    raise ConfigError(msg)

get_path(key, default, *, namespace=PATHS_NAMESPACE)

Resolve key in [paths] as a normalised :class:~pathlib.Path.

default is the caller's existing constant and is returned unchanged when nothing is configured -- neither expanded nor validated -- so wiring a consumer up cannot alter today's behaviour.

A configured value (env or file) is expanded (~), resolved to an absolute path, and refused via :func:resolve_safe if it sits inside a git checkout. Raises :class:ConfigError on a non-string/non-path value or an in-repo path, so a bad config fails loudly at the boundary rather than writing runtime state somewhere unintended.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def get_path(
    key: str,
    default: Path,
    *,
    namespace: str = PATHS_NAMESPACE,
) -> Path:
    """Resolve ``key`` in ``[paths]`` as a normalised :class:`~pathlib.Path`.

    ``default`` is the caller's existing constant and is returned **unchanged**
    when nothing is configured -- neither expanded nor validated -- so wiring a
    consumer up cannot alter today's behaviour.

    A configured value (env or file) is expanded (``~``), resolved to an
    absolute path, and refused via :func:`resolve_safe` if it sits inside a git
    checkout. Raises :class:`ConfigError` on a non-string/non-path value or an
    in-repo path, so a bad config fails loudly at the boundary rather than
    writing runtime state somewhere unintended.
    """
    configured = _resolve_configured(namespace, key)
    if configured is _MISSING:
        return default
    if not isinstance(configured, str | Path):
        msg = (
            f"invalid path for {namespace}.{key}: "
            f"expected a string, got {type(configured).__name__}"
        )
        raise ConfigError(msg)
    expanded = Path(configured).expanduser()
    try:
        return resolve_safe(expanded)
    except ValueError as exc:
        msg = f"invalid path for {namespace}.{key}: {exc}"
        raise ConfigError(msg) from exc

get_str(key, default, *, namespace=PATHS_NAMESPACE)

Resolve a configured value as text while preserving an untouched default.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def get_str(
    key: str,
    default: str,
    *,
    namespace: str = PATHS_NAMESPACE,
) -> str:
    """Resolve a configured value as text while preserving an untouched default."""
    configured = _resolve_configured(namespace, key)
    if configured is _MISSING:
        return default
    return str(configured)

list_execution_policies()

Return valid policies in lexical order, skipping malformed leaves.

Source code in packages/axm-config/src/axm_config/resolver.py
Python
def list_execution_policies() -> dict[str, ExecutionPolicyOverride]:
    """Return valid policies in lexical order, skipping malformed leaves."""
    policies: dict[str, ExecutionPolicyOverride] = {}
    masked: set[str] = set()
    namespaces = _store.namespaces()

    for namespace in namespaces:
        if not namespace.startswith(_EXECUTION_NAMESPACE_PREFIX):
            continue
        entry = _canonical_policy_entry(namespace)
        if entry is None:
            continue
        ticket_type, policy = entry
        masked.add(ticket_type)
        if policy is not None:
            policies[ticket_type] = policy

    for namespace in namespaces:
        if not namespace.startswith("execution.") or namespace.startswith(
            _EXECUTION_NAMESPACE_PREFIX
        ):
            continue
        entry = _legacy_policy_entry(namespace)
        if entry is None:
            continue
        ticket_type, policy = entry
        if ticket_type not in masked:
            policies[ticket_type] = policy

    return dict(sorted(policies.items()))

load(namespace, model)

Build model from namespace, resolving each field by name.

Every field of model is resolved via :func:get (the field name is the config key). Unresolved fields are omitted so pydantic applies the field default; a required field that stays unresolved raises :class:ConfigError instead of a raw ValidationError.

Source code in packages/axm-config/src/axm_config/resolver.py
Python
def load[M: BaseModel](namespace: str, model: type[M]) -> M:
    """Build ``model`` from ``namespace``, resolving each field by name.

    Every field of ``model`` is resolved via :func:`get` (the field name is
    the config key). Unresolved fields are omitted so pydantic applies the
    field default; a required field that stays unresolved raises
    :class:`ConfigError` instead of a raw ``ValidationError``.
    """
    values: dict[str, object] = {}
    for field in model.model_fields:
        resolved = resolve(namespace, field, _MISSING)
        if resolved is not _MISSING:
            values[field] = resolved
    try:
        return model.model_validate(values)
    except Exception as exc:
        msg = f"cannot build {model.__name__} for namespace {namespace!r}: {exc}"
        raise ConfigError(msg) from exc

protocols_dir(*, default=None)

The legacy YAML protocol directory read by the engine and briefings.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def protocols_dir(*, default: Path | None = None) -> Path:
    """The legacy YAML protocol directory read by the engine and briefings."""
    fallback = default if default is not None else Path.home() / "axm" / "protocols"
    return get_path("protocols_dir", default=fallback)

quality_dir(*, default=None)

The quality-trace directory written by axm-audit / axm-init.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def quality_dir(*, default: Path | None = None) -> Path:
    """The quality-trace directory written by ``axm-audit`` / ``axm-init``."""
    fallback = default if default is not None else Path.home() / "axm" / "quality"
    return get_path("quality_dir", default=fallback)

resolve_safe(target)

Resolve target and refuse any path sitting inside a git repo.

Walks the resolved path and its ancestors looking for a .git marker (a source checkout). Raises :class:ValueError rather than returning an in-repo path; returns the resolved path otherwise.

Source code in packages/axm-config/src/axm_config/home.py
Python
def resolve_safe(target: Path | str) -> Path:
    """Resolve ``target`` and refuse any path sitting inside a git repo.

    Walks the resolved path and its ancestors looking for a ``.git`` marker
    (a source checkout). Raises :class:`ValueError` rather than returning an
    in-repo path; returns the resolved path otherwise.
    """
    resolved = Path(target).resolve()
    for ancestor in (resolved, *resolved.parents):
        if (ancestor / ".git").exists():
            msg = (
                f"refusing in-repo path {resolved}: "
                f"resolves inside the git checkout at {ancestor}"
            )
            raise ValueError(msg)
    return resolved

sessions_root(*, default=None)

The loom sessions root -- where runs write manifests, traces, artifacts.

Declared identically in axm-loom, axm-knowledge and axm-orison (whose docstrings already say they mirror loom); this is the seam those three delegate to. default overrides the built-in ~/axm/sessions for a caller that must keep its own constant during migration.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def sessions_root(*, default: Path | None = None) -> Path:
    """The loom sessions root -- where runs write manifests, traces, artifacts.

    Declared identically in ``axm-loom``, ``axm-knowledge`` and ``axm-orison``
    (whose docstrings already say they *mirror* loom); this is the seam those
    three delegate to. ``default`` overrides the built-in ``~/axm/sessions``
    for a caller that must keep its own constant during migration.
    """
    fallback = default if default is not None else Path.home() / "axm" / "sessions"
    return get_path("sessions_root", default=fallback)

set_(namespace, key, value)

Persist key = value in the [namespace] section of config.toml.

namespace and key are validated against the safe-segment pattern first (path-traversal guard). A value of None is routed to :func:delete — TOML cannot encode None, so deleting the key is the well-defined contract rather than a raw TypeError. Otherwise delegates to :meth:NamespaceStore.write (atomic, 0600, other keys preserved).

Source code in packages/axm-config/src/axm_config/resolver.py
Python
def set_(namespace: str, key: str, value: object) -> None:
    """Persist ``key`` = ``value`` in the ``[namespace]`` section of config.toml.

    ``namespace`` and ``key`` are validated against the safe-segment pattern
    first (path-traversal guard). A ``value`` of ``None`` is routed to
    :func:`delete` — TOML cannot encode ``None``, so deleting the key is the
    well-defined contract rather than a raw ``TypeError``. Otherwise delegates
    to :meth:`NamespaceStore.write` (atomic, ``0600``, other keys preserved).
    """
    validate_segment(namespace, kind="namespace")
    validate_segment(key, kind="key")
    if value is None:
        _store.delete(namespace, key)
        return
    _store.write(namespace, key, value)

set_execution_policy(ticket_type, *, backend=None, model=None, analysis_enabled=None)

Atomically replace or clear one complete ticket-type policy leaf.

Source code in packages/axm-config/src/axm_config/resolver.py
Python
def set_execution_policy(
    ticket_type: str,
    *,
    backend: str | None = None,
    model: str | None = None,
    analysis_enabled: bool | None = None,
) -> None:
    """Atomically replace or clear one complete ticket-type policy leaf."""
    namespace = _execution_namespace(ticket_type)
    if backend is None and model is None and analysis_enabled is None:
        _store.replace_section(namespace, {"tombstone": "v1"})
        return
    policy = _policy_from_values(
        {
            "backend": backend,
            "model": model,
            "analysis_enabled": analysis_enabled,
        },
        layer="argument",
    )
    _store.replace_section(
        namespace,
        policy.model_dump(exclude_none=True),
    )

validate_segment(value, *, kind='segment')

Return value if it is a safe config segment, else raise ConfigError.

A segment is a namespace or a key: the single entry-point guard against path traversal and env-name ambiguity. It must be a non-empty str matching its kind's pattern — no path separators, no .. traversal, no NUL byte — so it can never widen the on-disk ~/.axm/<ns>.toml path. Both patterns are lowercase-only (no upper-case): the env-name surface upper-cases the segments, so accepting both "Demo" and "demo" would let two distinct namespaces fold to the same AXM_DEMO_* prefix — forbidding upper-case makes that collision unrepresentable. The patterns differ by kind: a "namespace" (:data:_NAMESPACE_RE) is lowercase-alphanumeric segments joined by dots — no _ and no - — whereas a "key" (:data:_KEY_RE) is lowercase-alphanumeric segments joined by single _ (no ./-, no leading/trailing _, no doubled __) so the derived env name stays POSIX-valid and the ns/key boundary is unambiguous: only the namespace's dot-fold yields __, the key can never forge one, and the lone single _ separates the folded namespace from the key. Any other kind falls back to the namespace pattern. Shared with every public boundary (and reused by the env-name surface) so validation is declared exactly once.

Source code in packages/axm-config/src/axm_config/resolver.py
Python
def validate_segment(value: str, *, kind: str = "segment") -> str:
    """Return ``value`` if it is a safe config segment, else raise ConfigError.

    A *segment* is a ``namespace`` or a ``key``: the single entry-point guard
    against path traversal and env-name ambiguity. It must be a non-empty
    ``str`` matching its kind's pattern — no path separators, no ``..``
    traversal, no NUL byte — so it can never widen the on-disk
    ``~/.axm/<ns>.toml`` path. Both patterns are **lowercase-only** (no
    upper-case): the env-name surface upper-cases the segments, so accepting
    both ``"Demo"`` and ``"demo"`` would let two distinct namespaces fold to
    the *same* ``AXM_DEMO_*`` prefix — forbidding upper-case makes that
    collision unrepresentable. The patterns differ by ``kind``: a
    ``"namespace"`` (:data:`_NAMESPACE_RE`) is lowercase-alphanumeric segments
    joined by dots — no ``_`` and no ``-`` — whereas a ``"key"``
    (:data:`_KEY_RE`) is lowercase-alphanumeric segments joined by **single**
    ``_`` (no ``.``/``-``, no leading/trailing ``_``, no doubled ``__``) so the
    derived env name stays POSIX-valid and the ns/key boundary is
    unambiguous: only the namespace's dot-fold yields ``__``, the key can
    never forge one, and the lone single ``_`` separates the folded namespace
    from the key. Any other ``kind`` falls back to the namespace pattern.
    Shared with every public boundary (and reused by the env-name surface) so
    validation is declared exactly once.
    """
    pattern = _SEGMENT_PATTERNS.get(kind, _NAMESPACE_RE)
    if not isinstance(value, str) or not pattern.match(value):
        msg = f"invalid {kind} {value!r}: must match {pattern.pattern}"
        raise ConfigError(msg)
    return value

warden_autostart(*, default=None)

Return whether consumers should start the warden automatically.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def warden_autostart(*, default: bool | None = None) -> bool:
    """Return whether consumers should start the warden automatically."""
    fallback = default if default is not None else _DEFAULT_WARDEN_AUTOSTART
    return get_bool("autostart", fallback, namespace=_WARDEN_NAMESPACE)

warden_binary_path(*, default=None)

Return the configured or interpreter-relative warden executable path.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def warden_binary_path(*, default: Path | None = None) -> Path:
    """Return the configured or interpreter-relative warden executable path."""
    fallback = (
        default if default is not None else Path(sys.executable).parent / "axm-warden"
    )
    return get_path("binary_path", fallback, namespace=_WARDEN_NAMESPACE)

warden_log_path(*, default=None)

Return the configured or AXM-home-relative warden log path.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def warden_log_path(*, default: Path | None = None) -> Path:
    """Return the configured or AXM-home-relative warden log path."""
    fallback = default if default is not None else axm_home() / "warden.log"
    return get_path("log_path", fallback, namespace=_WARDEN_NAMESPACE)

warden_max_concurrent(*, default=None)

Return the strictly positive warden concurrency limit.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def warden_max_concurrent(*, default: int | None = None) -> int:
    """Return the strictly positive warden concurrency limit."""
    fallback = default if default is not None else _DEFAULT_WARDEN_MAX_CONCURRENT
    configured = _resolve_configured(_WARDEN_NAMESPACE, "max_concurrent")
    if configured is _MISSING:
        return fallback

    value = get_int("max_concurrent", fallback, namespace=_WARDEN_NAMESPACE)
    if value <= 0:
        msg = f"invalid value for warden.max_concurrent: expected > 0, got {value}"
        raise ConfigError(msg)
    return value

warden_mode(*, default=None)

Return the configured warden execution mode.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def warden_mode(*, default: str | None = None) -> str:
    """Return the configured warden execution mode."""
    fallback = default if default is not None else _DEFAULT_WARDEN_MODE
    configured = _resolve_configured(_WARDEN_NAMESPACE, "mode")
    if configured is _MISSING:
        return fallback

    value = get_str("mode", fallback, namespace=_WARDEN_NAMESPACE)
    if value not in _WARDEN_MODES:
        expected = ", ".join(sorted(_WARDEN_MODES))
        msg = (
            f"invalid value for warden.mode: expected one of {expected}, got {value!r}"
        )
        raise ConfigError(msg)
    return value

warden_socket(*, default=None)

The warden control-plane socket bound by axm-warden serve.

Note the precedence a consumer must preserve. Callers layer an explicit argument on top of this (--socket on the CLI, the socket= kwarg on the tools), which outranks everything here; this function covers only the env > file > default tail below it. Collapsing the explicit argument into the config lookup would silently change behaviour, so callers keep their own if socket is not None: return socket guard and delegate the rest.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def warden_socket(*, default: Path | None = None) -> Path:
    """The warden control-plane socket bound by ``axm-warden serve``.

    Note the precedence a consumer must preserve. Callers layer an *explicit
    argument* on top of this (``--socket`` on the CLI, the ``socket=`` kwarg on
    the tools), which outranks everything here; this function covers only the
    ``env > file > default`` tail below it. Collapsing the explicit argument
    into the config lookup would silently change behaviour, so callers keep
    their own ``if socket is not None: return socket`` guard and delegate the
    rest.
    """
    fallback = default if default is not None else Path.home() / ".axm" / "warden.sock"
    return get_path("warden_socket", default=fallback)