host.controller_web_editor.paths
Path resolution chokepoint.
All file paths accepted from the web client flow through safe_resolve.
v1 allowlists data/ under the repo root. Widening the scope later is a
one-line change here.
1"""Path resolution chokepoint. 2 3All file paths accepted from the web client flow through ``safe_resolve``. 4v1 allowlists ``data/`` under the repo root. Widening the scope later is a 5one-line change here. 6""" 7 8from pathlib import Path 9 10 11REPO_ROOT = Path(__file__).resolve().parents[2] 12 13# Directories under REPO_ROOT that the editor may read/write. 14ALLOWED_ROOTS: tuple[Path, ...] = ( 15 REPO_ROOT / "data", 16) 17 18 19class PathNotAllowedError(ValueError): 20 """Raised when a requested path falls outside the allowlist.""" 21 22 23def safe_resolve(repo_relative: str) -> Path: 24 """Resolve a client-supplied path against the repo root. 25 26 The path must be relative and, after resolution, must live inside one 27 of ``ALLOWED_ROOTS``. Symlinks are followed; traversal via ``..`` is 28 rejected because the resolved path leaves the allowed root. 29 """ 30 if not repo_relative: 31 raise PathNotAllowedError("empty path") 32 candidate = (REPO_ROOT / repo_relative).resolve() 33 for root in ALLOWED_ROOTS: 34 try: 35 candidate.relative_to(root) 36 except ValueError: 37 continue 38 return candidate 39 raise PathNotAllowedError( 40 f"path '{repo_relative}' is outside the allowed roots") 41 42 43def list_yaml_configs() -> list[str]: 44 """Return repo-relative YAML paths under every allowed root.""" 45 results: list[str] = [] 46 for root in ALLOWED_ROOTS: 47 if not root.is_dir(): 48 continue 49 for p in sorted(root.rglob("*.yaml")): 50 results.append(str(p.relative_to(REPO_ROOT)).replace("\\", "/")) 51 return results
REPO_ROOT =
PosixPath('/home/runner/work/Robot-2026/Robot-2026')
ALLOWED_ROOTS: tuple[pathlib.Path, ...] =
(PosixPath('/home/runner/work/Robot-2026/Robot-2026/data'),)
class
PathNotAllowedError(builtins.ValueError):
20class PathNotAllowedError(ValueError): 21 """Raised when a requested path falls outside the allowlist."""
Raised when a requested path falls outside the allowlist.
def
safe_resolve(repo_relative: str) -> pathlib.Path:
24def safe_resolve(repo_relative: str) -> Path: 25 """Resolve a client-supplied path against the repo root. 26 27 The path must be relative and, after resolution, must live inside one 28 of ``ALLOWED_ROOTS``. Symlinks are followed; traversal via ``..`` is 29 rejected because the resolved path leaves the allowed root. 30 """ 31 if not repo_relative: 32 raise PathNotAllowedError("empty path") 33 candidate = (REPO_ROOT / repo_relative).resolve() 34 for root in ALLOWED_ROOTS: 35 try: 36 candidate.relative_to(root) 37 except ValueError: 38 continue 39 return candidate 40 raise PathNotAllowedError( 41 f"path '{repo_relative}' is outside the allowed roots")
Resolve a client-supplied path against the repo root.
The path must be relative and, after resolution, must live inside one
of ALLOWED_ROOTS. Symlinks are followed; traversal via .. is
rejected because the resolved path leaves the allowed root.
def
list_yaml_configs() -> list[str]:
44def list_yaml_configs() -> list[str]: 45 """Return repo-relative YAML paths under every allowed root.""" 46 results: list[str] = [] 47 for root in ALLOWED_ROOTS: 48 if not root.is_dir(): 49 continue 50 for p in sorted(root.rglob("*.yaml")): 51 results.append(str(p.relative_to(REPO_ROOT)).replace("\\", "/")) 52 return results
Return repo-relative YAML paths under every allowed root.