Skip to content

paths

paths

Platform-aware path resolution for the terok ecosystem.

Provides generic namespace resolvers that any sibling package can call to place its state/config/runtime under the shared terok/ namespace.

Resolution priority for each resolver:

  1. Package-specific override (env_var argument)
  2. TEROK_ROOT env var (namespace-wide override; state only)
  3. Platform default (FHS root paths or XDG dirs via platformdirs)

Layered config-file reading (/etc/terok/config.yml~/.config/terok/config.yml) is a sibling-package concern — packages that want it compose ConfigStack against these resolvers themselves.

__all__ = ['config_file_paths', 'host_uid', 'namespace_config_dir', 'namespace_runtime_dir', 'namespace_state_dir', 'read_config_section', 'read_config_top_level'] module-attribute

host_uid()

Return the current process's UID as the initial user namespace sees it.

Inside an unprivileged user namespace (rootless podman / crun hook, sandboxed CI runner, unshare -U), os.geteuid() returns the inner-userns UID — typically 0 even when the operator really ran the program as UID 1000. Network peers (D-Bus SO_PEERCRED, AUTH EXTERNAL) and kernel-level checks see the outer (host) UID via the userns uid_map translation, so a process that advertises its inner UID over the wire is rejected for a credential mismatch. This helper hands callers the outer UID those peers expect.

The mapping comes from /proc/self/uid_map. When it is unavailable (macOS, BSD, exotic chroot) or no row covers the effective UID, the bare geteuid() answer is returned — correct on systems without Linux user namespaces.

Source code in src/terok_util/paths.py
def host_uid() -> int:
    """Return the current process's UID as the initial user namespace sees it.

    Inside an unprivileged user namespace (rootless ``podman`` / ``crun``
    hook, sandboxed CI runner, ``unshare -U``), ``os.geteuid()`` returns
    the *inner*-userns UID — typically ``0`` even when the operator
    really ran the program as UID 1000.  Network peers (D-Bus
    ``SO_PEERCRED``, ``AUTH EXTERNAL``) and kernel-level checks see the
    *outer* (host) UID via the userns ``uid_map`` translation, so a
    process that advertises its inner UID over the wire is rejected for
    a credential mismatch.  This helper hands callers the outer UID those
    peers expect.

    The mapping comes from ``/proc/self/uid_map``.  When it is
    unavailable (macOS, BSD, exotic chroot) or no row covers the
    effective UID, the bare ``geteuid()`` answer is returned — correct on
    systems without Linux user namespaces.
    """
    try:
        euid = os.geteuid()
    except AttributeError:
        return 0 if getpass.getuser() == "root" else -1
    try:
        with open("/proc/self/uid_map", encoding="ascii") as fh:
            for line in fh:
                parts = line.split()
                if len(parts) != 3:
                    continue
                try:
                    inner_start, outer_start, length = (int(p) for p in parts)
                except ValueError:
                    continue
                if inner_start <= euid < inner_start + length:
                    return outer_start + (euid - inner_start)
    except OSError:
        pass
    return euid

config_file_paths()

Ordered config.yml locations with scope labels (lowest → highest priority).

TEROK_CONFIG_FILE → single override (no layering). Otherwise: /etc/terok/config.yml (system) → ~/.config/terok/config.yml (user). Root processes see only the system path.

Public so consumers can render an "edit one of these to override X" hint to the operator (which file gets the highest priority, where on disk the operator would put the override, etc.).

Source code in src/terok_util/paths.py
def config_file_paths() -> list[tuple[str, Path]]:
    """Ordered config.yml locations with scope labels (lowest → highest priority).

    ``TEROK_CONFIG_FILE`` → single override (no layering).  Otherwise:
    ``/etc/terok/config.yml`` (system) → ``~/.config/terok/config.yml``
    (user).  Root processes see only the system path.

    Public so consumers can render an "edit one of these to override X"
    hint to the operator (which file gets the highest priority, where
    on disk the operator would put the override, etc.).
    """
    env = os.getenv(_TEROK_CONFIG_FILE_ENV)
    if env:
        return [("override", Path(env).expanduser())]
    result: list[tuple[str, Path]] = [
        ("system", Path("/etc") / _NAMESPACE / "config.yml"),
    ]
    if not _is_root():
        if _user_config_dir is not None:
            user_base = Path(_user_config_dir(_NAMESPACE))
        else:
            user_base = Path.home() / ".config" / _NAMESPACE
        result.append(("user", user_base / "config.yml"))
    return result

read_config_section(section)

Read a top-level section from layered terok configs (cached, fail-silent).

Merges system and user config.yml files via ConfigStack — user values override system defaults at the leaf level. Lazy-imports config_stack so importing paths doesn't drag the YAML parser into a process that only needs the platform defaults.

Source code in src/terok_util/paths.py
def read_config_section(section: str) -> dict[str, str]:
    """Read a top-level section from layered terok configs (cached, fail-silent).

    Merges system and user ``config.yml`` files via
    [`ConfigStack`][terok_util.config_stack.ConfigStack] — user values
    override system defaults at the leaf level.  Lazy-imports
    ``config_stack`` so importing ``paths`` doesn't drag the YAML
    parser into a process that only needs the platform defaults.
    """
    if section in _config_section_cache:
        return _config_section_cache[section]

    result: dict[str, str] = {}
    try:
        from .config_stack import ConfigStack, load_yaml_scope

        stack = ConfigStack()
        for label, path in config_file_paths():
            stack.push(load_yaml_scope(label, path))
        merged = stack.resolve_section(section)
        result = {k: str(v) for k, v in merged.items() if v is not None}
    except Exception:  # noqa: BLE001 — fail-silent; bad config should not crash path resolution  # nosec B110 — best-effort probe; failure is non-fatal
        pass
    _config_section_cache[section] = result
    return result

read_config_top_level(key)

Read a top-level scalar / list / mapping from layered terok configs.

Counterpart to read_config_section for keys whose value isn't a dict — e.g. the ecosystem-wide experimental: true opt-in or a bare log_level: debug knob. Returns the merged value (user wins over system) or None when the key is absent or the config files can't be loaded. Cached for the lifetime of the process; reaches for the _config_top_level_cache private to flush in tests.

Source code in src/terok_util/paths.py
def read_config_top_level(key: str) -> object | None:
    """Read a top-level scalar / list / mapping from layered terok configs.

    Counterpart to
    [`read_config_section`][terok_util.paths.read_config_section] for
    keys whose value isn't a dict — e.g. the ecosystem-wide
    ``experimental: true`` opt-in or a bare ``log_level: debug`` knob.
    Returns the merged value (user wins over system) or ``None`` when
    the key is absent or the config files can't be loaded.  Cached for
    the lifetime of the process; reaches for the ``_config_top_level_cache``
    private to flush in tests.
    """
    if key in _config_top_level_cache:
        return _config_top_level_cache[key]

    result: object | None = None
    try:
        from .config_stack import ConfigStack, load_yaml_scope

        stack = ConfigStack()
        for label, path in config_file_paths():
            stack.push(load_yaml_scope(label, path))
        result = stack.resolve().get(key)
    except Exception:  # noqa: BLE001 — fail-silent; bad config should not crash field resolution  # nosec B110 — best-effort probe; failure is non-fatal
        pass
    _config_top_level_cache[key] = result
    return result

namespace_state_dir(subdir='', *, env_var=None)

Resolve a state directory under the terok/ namespace.

Priority:

  1. env_var (package-specific override, e.g. TEROK_SANDBOX_STATE_DIR)
  2. TEROK_ROOT env var (namespace override)
  3. config.ymlpaths.root (Podman model — all packages honour it)
  4. Platform default (/var/lib/terok/<subdir> for root, XDG data dir otherwise)

env_var is keyword-only so a positional second argument can never accidentally be reinterpreted as an override name.

Source code in src/terok_util/paths.py
def namespace_state_dir(subdir: str = "", *, env_var: str | None = None) -> Path:
    """Resolve a state directory under the ``terok/`` namespace.

    Priority:

    1. *env_var* (package-specific override, e.g. ``TEROK_SANDBOX_STATE_DIR``)
    2. ``TEROK_ROOT`` env var (namespace override)
    3. ``config.yml`` → ``paths.root`` (Podman model — all packages honour it)
    4. Platform default (``/var/lib/terok/<subdir>`` for root, XDG data
       dir otherwise)

    *env_var* is keyword-only so a positional second argument can never
    accidentally be reinterpreted as an override name.
    """
    if env_var:
        val = os.getenv(env_var)
        if val:
            return Path(val).expanduser()
    root = _namespace_root()
    base = root if root else _platform_state_base()
    return _safe_subdir(base, subdir)

namespace_config_dir(subdir='', *, env_var=None)

Resolve a config directory under the terok/ namespace.

Priority: env_var/etc/terok/<subdir> (root) → platformdirs → ~/.config/terok/<subdir>. env_var is keyword-only.

Source code in src/terok_util/paths.py
def namespace_config_dir(subdir: str = "", *, env_var: str | None = None) -> Path:
    """Resolve a config directory under the ``terok/`` namespace.

    Priority: *env_var* → ``/etc/terok/<subdir>`` (root) → platformdirs
    → ``~/.config/terok/<subdir>``.  *env_var* is keyword-only.
    """
    if env_var:
        val = os.getenv(env_var)
        if val:
            return Path(val).expanduser()
    base: Path
    if _is_root():
        base = Path("/etc") / _NAMESPACE
    elif _user_config_dir is not None:
        base = Path(_user_config_dir(_NAMESPACE))
    else:
        base = Path.home() / ".config" / _NAMESPACE
    return _safe_subdir(base, subdir)

namespace_runtime_dir(subdir='', *, env_var=None)

Resolve a runtime directory under the terok/ namespace.

Priority: env_var/run/terok/<subdir> (root) → $XDG_RUNTIME_DIR/terok/<subdir>$XDG_STATE_HOME/terok/<subdir>~/.local/state/terok/<subdir>. env_var is keyword-only.

Source code in src/terok_util/paths.py
def namespace_runtime_dir(subdir: str = "", *, env_var: str | None = None) -> Path:
    """Resolve a runtime directory under the ``terok/`` namespace.

    Priority: *env_var* → ``/run/terok/<subdir>`` (root)
    → ``$XDG_RUNTIME_DIR/terok/<subdir>`` → ``$XDG_STATE_HOME/terok/<subdir>``
    → ``~/.local/state/terok/<subdir>``.  *env_var* is keyword-only.
    """
    if env_var:
        val = os.getenv(env_var)
        if val:
            return Path(val).expanduser()
    base: Path
    if _is_root():
        base = Path("/run") / _NAMESPACE
    else:
        xdg_runtime = os.getenv("XDG_RUNTIME_DIR")
        if xdg_runtime:
            base = Path(xdg_runtime) / _NAMESPACE
        else:
            xdg_state = os.getenv("XDG_STATE_HOME")
            base = (
                Path(xdg_state) / _NAMESPACE
                if xdg_state
                else Path.home() / ".local" / "state" / _NAMESPACE
            )
    return _safe_subdir(base, subdir)