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`.
        """
        from axm_config.profile import profile_config_path

        home = _safe_home()
        path = profile_config_path().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 = (self._config_path().parent / 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()))
        store_root = self._config_path().parent
        for legacy in store_root.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``.
        """
        axm_home()
        path = self._config_path()
        path.parent.mkdir(parents=True, exist_ok=True)
        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()))
    store_root = self._config_path().parent
    for legacy in store_root.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)

ProfileIsolation

Bases: BaseModel

Resolved state paths and their isolation verdict for one profile.

Source code in packages/axm-config/src/axm_config/isolation.py
Python
class ProfileIsolation(BaseModel):  # type: ignore[explicit-any]
    """Resolved state paths and their isolation verdict for one profile."""

    profile: str
    profile_root: Path
    paths: dict[str, Path]
    isolated: bool
    escapes: list[str]

ProfileIsolationTool

Resolve isolated state paths for an explicit or active profile.

Source code in packages/axm-config/src/axm_config/tools.py
Python
class ProfileIsolationTool:
    """Resolve isolated state paths for an explicit or active profile."""

    agent_hint = (
        "Resolve the six isolated state paths for a profile without creating "
        "directories or mutating configuration."
    )
    domain = "config"
    tags = frozenset({"config", "profile", "isolation"})

    @property
    def name(self) -> str:
        """Unique tool identifier."""
        return "profile_isolation"

    def execute(self, *, profile: str | None = None) -> ToolResult:
        """Return resolved profile paths and their isolation verdict."""
        try:
            isolation = profile_isolation(profile)
        except Exception as exc:  # noqa: BLE001 - AXMTool boundary
            return ToolResult(success=False, error=str(exc))

        data: dict[str, object] = {
            name: str(path) for name, path in isolation.paths.items()
        }
        data["profile"] = isolation.profile
        data["isolated"] = isolation.isolated
        text = "\n".join(f"{name}: {path}" for name, path in isolation.paths.items())
        return ToolResult(success=True, data=data, text=text)
name property

Unique tool identifier.

execute(*, profile=None)

Return resolved profile paths and their isolation verdict.

Source code in packages/axm-config/src/axm_config/tools.py
Python
def execute(self, *, profile: str | None = None) -> ToolResult:
    """Return resolved profile paths and their isolation verdict."""
    try:
        isolation = profile_isolation(profile)
    except Exception as exc:  # noqa: BLE001 - AXMTool boundary
        return ToolResult(success=False, error=str(exc))

    data: dict[str, object] = {
        name: str(path) for name, path in isolation.paths.items()
    }
    data["profile"] = isolation.profile
    data["isolated"] = isolation.isolated
    text = "\n".join(f"{name}: {path}" for name, path in isolation.paths.items())
    return ToolResult(success=True, data=data, text=text)

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 = axm_home_path()
    home.mkdir(mode=AXM_DIR_MODE, parents=True, exist_ok=True)
    if os.name == "posix":
        os.chmod(home, AXM_DIR_MODE)
    return home

axm_home_path()

Return the resolved ~/.axm path without touching the filesystem.

Pure computation, the read-only counterpart of :func:axm_home: it never creates the directory nor tightens its permissions, so merely describing or resolving a location (a report, a path lookup) no longer materialises the home. Every code path that actually persists state keeps calling :func:axm_home.

Source code in packages/axm-config/src/axm_config/home.py
Python
def axm_home_path() -> Path:
    """Return the resolved ``~/.axm`` path without touching the filesystem.

    Pure computation, the read-only counterpart of :func:`axm_home`: it never
    creates the directory nor tightens its permissions, so merely *describing*
    or resolving a location (a report, a path lookup) no longer materialises
    the home. Every code path that actually persists state keeps calling
    :func:`axm_home`.
    """
    return (Path.home() / ".axm").resolve()

current_profile()

Return the active, lexically validated state profile.

Source code in packages/axm-config/src/axm_config/profile.py
Python
def current_profile() -> str:
    """Return the active, lexically validated state profile."""
    profile = os.environ.get(PROFILE_ENV_VAR) or DEFAULT_PROFILE
    return validate_profile_name(profile)

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_file(namespace, key, *, default=None)

Return a persisted value without consulting environment overrides.

Source code in packages/axm-config/src/axm_config/resolver.py
Python
def get_file(namespace: str, key: str, *, default: object = None) -> object:
    """Return a persisted value without consulting environment overrides."""
    validate_segment(namespace, kind="namespace")
    validate_segment(key, kind="key")
    return _get_file_value(namespace, key, default)

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

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,
    env_aliases: Sequence[str] = (),
) -> int:
    """Resolve a configured integer while preserving an untouched default."""
    configured = _resolve_configured(namespace, key, env_aliases=env_aliases)
    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, profile=None)

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

default is the caller's existing constant. In production it is returned unchanged when nothing is configured. For the state paths registered in _PROFILE_RELATIVE_PATHS, a non-production profile replaces that fallback with a path rooted below :func:profile_root.

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,
    profile: str | None = None,
) -> Path:
    """Resolve ``key`` in ``[paths]`` as a normalised :class:`~pathlib.Path`.

    ``default`` is the caller's existing constant. In production it is returned
    **unchanged** when nothing is configured. For the state paths registered in
    ``_PROFILE_RELATIVE_PATHS``, a non-production profile replaces that fallback
    with a path rooted below :func:`profile_root`.

    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)
    requested = _requested_profile(profile)
    root = profile_root_for(requested)
    if configured is _MISSING:
        relative = _PROFILE_RELATIVE_PATHS.get((namespace, key))
        if root is not None and relative is not None:
            return root / relative
        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:
        resolved = resolve_safe(expanded)
    except ValueError as exc:
        msg = f"invalid path for {namespace}.{key}: {exc}"
        raise ConfigError(msg) from exc

    if root is not None:
        resolved_root = root.expanduser().resolve()
        if not resolved.is_relative_to(resolved_root):
            msg = (
                f"invalid path for {namespace}.{key}: profile {requested!r} "
                f"requires containment under {resolved_root}, got {resolved}"
            )
            raise ConfigError(msg)
    return resolved

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)

inference_base_url()

Return the configured inference engine address unchanged.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def inference_base_url() -> str:
    """Return the configured inference engine address unchanged."""
    return get_str(
        "base_url",
        _DEFAULT_INFERENCE_BASE_URL,
        namespace=_INFERENCE_NAMESPACE,
    )

inference_model()

Return the configured inference model identifier unchanged.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def inference_model() -> str:
    """Return the configured inference model identifier unchanged."""
    return get_str(
        "model",
        _DEFAULT_INFERENCE_MODEL,
        namespace=_INFERENCE_NAMESPACE,
    )

inference_origin()

Return the configured inference provider origin.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def inference_origin() -> str:
    """Return the configured inference provider origin."""
    value = get_str(
        "origin",
        _DEFAULT_INFERENCE_ORIGIN,
        namespace=_INFERENCE_NAMESPACE,
    )
    if value not in _INFERENCE_ORIGINS:
        expected = ", ".join(sorted(_INFERENCE_ORIGINS))
        msg = (
            "invalid value for inference.origin: "
            f"expected one of {expected}, got {value!r}"
        )
        raise ConfigError(msg)
    return value

is_isolated(root, paths)

Return whether every named path is contained by the root.

Source code in packages/axm-config/src/axm_config/isolation.py
Python
def is_isolated(
    root: Path,
    paths: Mapping[str, Path],
) -> tuple[bool, list[str]]:
    """Return whether every named path is contained by the root."""
    escapes = sorted(
        key for key, path in paths.items() if not path.is_relative_to(root)
    )
    return not escapes, escapes

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

profile_config_path()

Return the config store path for the active profile.

Source code in packages/axm-config/src/axm_config/profile.py
Python
def profile_config_path() -> Path:
    """Return the config store path for the active profile."""
    root = profile_root()
    if root is None:
        return axm_home_path() / "config.toml"
    return root / "config.toml"

profile_env()

Return the environment overlay that propagates the active profile.

Source code in packages/axm-config/src/axm_config/profile.py
Python
def profile_env() -> dict[str, str]:
    """Return the environment overlay that propagates the active profile."""
    return {PROFILE_ENV_VAR: current_profile()}

profile_isolation(profile=None)

Resolve a profile's state paths without creating filesystem entries.

Source code in packages/axm-config/src/axm_config/isolation.py
Python
def profile_isolation(profile: str | None = None) -> ProfileIsolation:
    """Resolve a profile's state paths without creating filesystem entries."""
    selected_profile = (
        validate_profile_name(profile) if profile is not None else current_profile()
    )
    root = axm_home_path() / "profiles" / selected_profile
    paths = _profile_paths(selected_profile)
    isolated, escapes = is_isolated(root, paths)
    return ProfileIsolation(
        profile=selected_profile,
        profile_root=root,
        paths=paths,
        isolated=isolated,
        escapes=escapes,
    )

profile_root()

Return the isolated state root, or None for production.

Source code in packages/axm-config/src/axm_config/profile.py
Python
def profile_root() -> Path | None:
    """Return the isolated state root, or None for production."""
    return profile_root_for(current_profile())

profile_root_for(profile)

Return the state root of profile, or None for the default one.

The named counterpart of :func:profile_root: it answers for an arbitrary profile instead of the active one, so a caller can ask what a profile would use without mutating AXM_PROFILE. Pure computation -- the root hangs below :func:axm_home_path, so no directory is created, and the deliberate asymmetry is preserved: AXM_HOME only selects the config.toml that is read, never this convention.

Source code in packages/axm-config/src/axm_config/profile.py
Python
def profile_root_for(profile: str) -> Path | None:
    """Return the state root of ``profile``, or None for the default one.

    The named counterpart of :func:`profile_root`: it answers for an arbitrary
    profile instead of the active one, so a caller can ask what a profile
    *would* use without mutating ``AXM_PROFILE``. Pure computation -- the root
    hangs below :func:`axm_home_path`, so no directory is created, and the
    deliberate asymmetry is preserved: ``AXM_HOME`` only selects the
    ``config.toml`` that is read, never this convention.
    """
    validated = validate_profile_name(profile)
    if validated == DEFAULT_PROFILE:
        return None
    return axm_home_path() / "profiles" / validated

protocols_dir(*, default=None, profile=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,
    profile: str | 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, profile=profile)

quality_dir(*, default=None, profile=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,
    profile: str | 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, profile=profile)

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

service_port(service, *, profile=None)

The TCP port service listens on for the active state profile.

A listening point is a profile-owned resource exactly like the state roots above, so the decision belongs here instead of being pushed onto every caller as "configure a port or I refuse to start".

Production is frozen: with nothing configured the adopted default is returned unchanged, so an existing installation reconfigures nothing. Under a non-production profile the fallback is derived from the profile name, so two installations on one machine do not fight over a port. Either way the value is only the default handed to :func:get_int, which keeps the resolver's env > file > default precedence intact.

A service may also carry historical environment variables, registered in _SERVICE_PORT_ENV_ALIASES: they are consulted after the derived name and before any configured value, so an installation still exporting the legacy variable keeps binding the port it always bound.

profile names the profile the question is asked for, exactly like :func:get_path and the state roots above: None means the active profile, and naming another one is a pure read -- it neither consults nor mutates the process-wide profile selection.

Raises :class:ConfigError naming service when it is not registered, before any resolution is attempted.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def service_port(service: str, *, profile: str | None = None) -> int:
    """The TCP port ``service`` listens on for the active state profile.

    A listening point is a profile-owned resource exactly like the state roots
    above, so the decision belongs here instead of being pushed onto every
    caller as "configure a port or I refuse to start".

    Production is frozen: with nothing configured the adopted default is
    returned unchanged, so an existing installation reconfigures nothing. Under
    a non-production profile the fallback is derived from the profile name, so
    two installations on one machine do not fight over a port. Either way the
    value is only the ``default`` handed to :func:`get_int`, which keeps the
    resolver's ``env > file > default`` precedence intact.

    A service may also carry historical environment variables, registered in
    ``_SERVICE_PORT_ENV_ALIASES``: they are consulted after the derived name
    and before any configured value, so an installation still exporting the
    legacy variable keeps binding the port it always bound.

    ``profile`` names the profile the question is asked for, exactly like
    :func:`get_path` and the state roots above: ``None`` means the active
    profile, and naming another one is a pure read -- it neither consults nor
    mutates the process-wide profile selection.

    Raises :class:`ConfigError` naming ``service`` when it is not registered,
    before any resolution is attempted.
    """
    decl = _SERVICES.get(service)
    if decl is None:
        known = ", ".join(sorted(_SERVICES))
        msg = f"unknown service id {service!r}: expected one of {known}"
        raise ConfigError(msg)

    requested = _requested_profile(profile)
    if profile_root_for(requested) is None:
        fallback = decl.adopted
    else:
        fallback = _derive_service_port(requested, service)
    return get_int(
        f"{service}_port",
        fallback,
        namespace=_NETWORK_NAMESPACE,
        env_aliases=_SERVICE_PORT_ENV_ALIASES.get(service, ()),
    )

sessions_root(*, default=None, profile=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. In production, default overrides the built-in ~/axm/sessions for callers retaining their migration constant; an active non-production profile instead owns the unconfigured state root.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def sessions_root(
    *,
    default: Path | None = None,
    profile: str | 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. In production, ``default`` overrides the built-in
    ``~/axm/sessions`` for callers retaining their migration constant; an
    active non-production profile instead owns the unconfigured state root.
    """
    fallback = default if default is not None else Path.home() / "axm" / "sessions"
    return get_path("sessions_root", default=fallback, profile=profile)

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),
    )

tickets_db(*, default=None, profile=None)

Return the ticket database path for the active state profile.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def tickets_db(
    *,
    default: Path | None = None,
    profile: str | None = None,
) -> Path:
    """Return the ticket database path for the active state profile."""
    active_root = profile_root_for(_requested_profile(profile))
    if default is not None:
        fallback = default
    elif active_root is None:
        fallback = Path.home() / "axm" / "tickets" / "tickets.db"
    else:
        fallback = active_root / "tickets" / "tickets.db"
    return get_path(
        "db_path",
        default=fallback,
        namespace="tickets",
        profile=profile,
    )

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, profile=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,
    profile: str | None = None,
) -> Path:
    """Return the configured or AXM-home-relative warden log path."""
    fallback = default if default is not None else axm_home_path() / "warden.log"
    return get_path(
        "log_path",
        fallback,
        namespace=_WARDEN_NAMESPACE,
        profile=profile,
    )

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_park_threshold(*, default=None)

Return the minimum number of failed generations before parking.

Source code in packages/axm-config/src/axm_config/paths.py
Python
def warden_park_threshold(*, default: int | None = None) -> int:
    """Return the minimum number of failed generations before parking."""
    fallback = default if default is not None else _DEFAULT_WARDEN_PARK_THRESHOLD
    configured = _resolve_configured(_WARDEN_NAMESPACE, "park_threshold")
    if configured is _MISSING:
        return fallback

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

warden_socket(*, default=None, profile=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,
    profile: str | 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, profile=profile)