paths.py
python
sha256:0e9549ec7b463911bc08b7d586dc320b1ac9b1f5c943ee7e3865dcc6cb0f6f83
chore(governance): sync handover+roadmap to 84db8c8 (drift:…
Human
8 hours ago
| 1 | """Repo-root resolution and path confinement (§K4.1 / §K4.9).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import os |
| 6 | from pathlib import Path |
| 7 | |
| 8 | |
| 9 | class PathEscapeError(Exception): |
| 10 | """Raised when a path would escape the resolved repo root.""" |
| 11 | |
| 12 | |
| 13 | def resolve_repo_root( |
| 14 | *, |
| 15 | cwd: Path, |
| 16 | repo_arg: str | None, |
| 17 | command: str, |
| 18 | ) -> Path: |
| 19 | """Resolve and absolutize the consumer repo root per §K4.1.""" |
| 20 | if repo_arg is not None: |
| 21 | return Path(repo_arg).expanduser().resolve() |
| 22 | |
| 23 | if command == "init": |
| 24 | return cwd.resolve() |
| 25 | |
| 26 | current = cwd.resolve() |
| 27 | for directory in (current, *current.parents): |
| 28 | if (directory / ".overseer").is_dir(): |
| 29 | return directory |
| 30 | return current |
| 31 | |
| 32 | |
| 33 | def resolve_config_path(repo_root: Path, config_arg: str | None) -> Path: |
| 34 | """Resolve the config file path; default ``<repo>/.overseer/config.yaml``.""" |
| 35 | if config_arg is not None: |
| 36 | path = Path(config_arg).expanduser() |
| 37 | if not path.is_absolute(): |
| 38 | path = repo_root / path |
| 39 | return path.resolve() |
| 40 | return (repo_root / ".overseer" / "config.yaml").resolve() |
| 41 | |
| 42 | |
| 43 | def repo_relative(repo_root: Path, path: Path) -> str: |
| 44 | """Return a POSIX repo-relative path, or ``.`` for the root.""" |
| 45 | resolved = path.resolve() |
| 46 | try: |
| 47 | rel = resolved.relative_to(repo_root.resolve()) |
| 48 | except ValueError: |
| 49 | raise PathEscapeError(f"path escapes repo root: {path}") from None |
| 50 | text = rel.as_posix() |
| 51 | return text if text else "." |
| 52 | |
| 53 | |
| 54 | def confine_path(repo_root: Path, user_path: str) -> Path: |
| 55 | """Resolve ``user_path`` under ``repo_root``; reject traversal escapes.""" |
| 56 | root = repo_root.resolve() |
| 57 | candidate = Path(user_path).expanduser() |
| 58 | if candidate.is_absolute(): |
| 59 | resolved = candidate.resolve() |
| 60 | else: |
| 61 | resolved = (root / candidate).resolve() |
| 62 | try: |
| 63 | resolved.relative_to(root) |
| 64 | except ValueError: |
| 65 | raise PathEscapeError(f"path escapes repo root: {user_path}") from None |
| 66 | return resolved |
| 67 | |
| 68 | |
| 69 | def is_within_repo(repo_root: Path, path: Path) -> bool: |
| 70 | """Return True if ``path`` is inside ``repo_root``.""" |
| 71 | try: |
| 72 | path.resolve().relative_to(repo_root.resolve()) |
| 73 | return True |
| 74 | except ValueError: |
| 75 | return False |
File History
1 commit
sha256:6abcf1fa82a7a621ccbc945f19acdba5bc0db54569599404a1452fb4a096a199
fix(ISR): default require_independent_second_reviewer to require
Human
minor
⚠
7 hours ago