From a96b540234613eb47b2f609a38ade0907f8d4e35 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Sun, 17 May 2026 12:48:50 +0200 Subject: [PATCH] feat(security): vault wall, path guard, and utils - utils/paths.py: pyra_home(), ensure_dir(), safe_chmod(), expand() - security/boundaries.py: VaultAccessError, PyraSecurityError, assert_safe_path() (called before every file read), check_vault_lock() Co-Authored-By: Claude Sonnet 4.6 --- src/pyra/security/boundaries.py | 41 +++++++++++++++++++++++++++++++++ src/pyra/utils/paths.py | 21 +++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/pyra/security/boundaries.py create mode 100644 src/pyra/utils/paths.py diff --git a/src/pyra/security/boundaries.py b/src/pyra/security/boundaries.py new file mode 100644 index 0000000..bb94244 --- /dev/null +++ b/src/pyra/security/boundaries.py @@ -0,0 +1,41 @@ +from pathlib import Path + +from pyra.utils.paths import pyra_home + + +class VaultAccessError(PermissionError): + pass + + +class PyraSecurityError(RuntimeError): + pass + + +VAULT_PATH: Path = pyra_home() / "vault" +BLOCKED_PREFIXES: list[Path] = [VAULT_PATH] + + +def assert_safe_path(path: Path) -> None: + """Raise VaultAccessError if path resolves into a blocked prefix.""" + resolved = path.resolve() + for blocked in BLOCKED_PREFIXES: + blocked_resolved = blocked.resolve() + try: + resolved.relative_to(blocked_resolved) + raise VaultAccessError( + f"Access denied: '{resolved}' is inside the protected vault. " + "The vault is never accessible to the AI." + ) + except ValueError: + pass + + +def check_vault_lock() -> None: + """Raise PyraSecurityError if the vault sentinel is missing.""" + lock = VAULT_PATH / ".vault_lock" + if not lock.exists(): + raise PyraSecurityError( + f"Vault sentinel missing: {lock}\n" + "This may indicate tampering. Refusing to start.\n" + "Run 'pyra setup' to reinitialise the vault." + ) diff --git a/src/pyra/utils/paths.py b/src/pyra/utils/paths.py new file mode 100644 index 0000000..3b72809 --- /dev/null +++ b/src/pyra/utils/paths.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + + +def pyra_home() -> Path: + return Path.home() / ".pyra" + + +def ensure_dir(path: Path, mode: int = 0o700) -> Path: + path.mkdir(parents=True, exist_ok=True) + safe_chmod(path, mode) + return path + + +def safe_chmod(path: Path, mode: int) -> None: + if os.name != "nt": + path.chmod(mode) + + +def expand(p: str) -> Path: + return Path(p).expanduser().resolve()