"""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