Newer
Older
navi-1 / mcp-servers / navi-3d / app / config.py
"""Configuration and path resolution for the navi-3d MCP server.

Paths are resolved inside ``session_files/<session_id>/`` when relative.
Absolute paths are accepted only if they sit inside ``FS_ALLOWED_PATHS``.
"""

from __future__ import annotations

import os
from pathlib import Path


class Settings:
    """Lightweight env-based settings."""

    def __init__(self) -> None:
        self.session_files_dir = Path(
            os.environ.get("SESSION_FILES_DIR", "session_files")
        ).resolve()
        self.fs_allowed_paths = self._parse_allowed_paths(
            os.environ.get("FS_ALLOWED_PATHS", "*")
        )
        self.openscad = os.environ.get("OPENSCAD_PATH", "openscad")

    def _parse_allowed_paths(self, raw: str) -> list[Path]:
        if raw == "*":
            return []
        return [Path(p.strip()).resolve() for p in raw.split(",") if p.strip()]

    def resolve_path(self, session_id: str, raw_path: str) -> Path:
        """Resolve a path that may be relative to the session directory.

        - Relative paths are resolved inside ``session_files/<session_id>/``.
        - Absolute paths are accepted only if they are inside one of the
          allowed roots (or ``*`` is set).

        Raises ``ValueError`` on anti-escape or disallowed absolute path.
        """
        path = Path(raw_path).expanduser()

        if not path.is_absolute():
            session_dir = (self.session_files_dir / session_id).resolve()
            path = session_dir / path
        else:
            path = path.resolve()
            if self.fs_allowed_paths:
                if not any(
                    path == allowed or path.is_relative_to(allowed)
                    for allowed in self.fs_allowed_paths
                ):
                    raise ValueError(
                        f"Absolute path {path} is outside allowed paths."
                    )

        # Anti-escape: the resolved path must still be inside the session dir
        # or inside an allowed root.
        session_dir = (self.session_files_dir / session_id).resolve()
        if not (path == session_dir or path.is_relative_to(session_dir)):
            if self.fs_allowed_paths:
                if not any(
                    path == allowed or path.is_relative_to(allowed)
                    for allowed in self.fs_allowed_paths
                ):
                    raise ValueError(f"Path escapes permitted directories: {path}")
            else:
                raise ValueError(f"Path escapes session directory: {path}")

        return path