Skip to content

Paths

paths

Shared filesystem roots, owned by axm-config and only read elsewhere.

The galaxy declared the same roots over and over: ~/axm/sessions in five packages, ~/axm/quality in three, ~/axm/protocols in three, and the ~/.axm/warden.sock resolution copy-pasted across four sites in two repos. Each copy is a place the value can silently drift. This module is the single owner: consumers call a getter here instead of rebuilding Path.home() / ....

Two properties make adoption safe, and they are the whole point:

  • The default stays in the caller's code. Every getter takes the caller's current constant as default, so with no ~/.axm/config.toml the resolved value is byte-for-byte what it is today. Wiring a package up is therefore purely additive -- no behaviour changes until someone actually configures a path.
  • Normalisation happens here, once. :func:get_path turns the resolver's raw value (an env var is always a str; a TOML value keeps its parsed type) into an expanded, resolved :class:~pathlib.Path. If each consumer did its own Path(...)/expanduser(), the duplication would simply move up one level instead of disappearing.

Precedence is the resolver's own env > file > default, so an existing override such as AXM_PATHS_WARDEN_SOCKET keeps working unchanged.

Security: a configured path is passed through :func:axm_config.home.resolve_safe, which refuses anything resolving inside a git checkout -- runtime state (session traces, quality reports, a socket) must never land in a repo where it can be committed. The default is never checked: it is code, not user input, and validating it would turn a working installation into a failing one, breaking the additive guarantee above.

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

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)

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)

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)