"""Exit-code contract and exception types for the Muse CLI.""" import enum class ExitCode(enum.IntEnum): """Standardised CLI exit codes. 0 — success 1 — user error (bad arguments, invalid input) 2 — repo-not-found / config invalid 3 — server / internal error 4 — requested item not found 5 — remote communication error 6 — partial success (completed with skipped/corrupt objects) """ SUCCESS = 0 USER_ERROR = 1 REPO_NOT_FOUND = 2 INTERNAL_ERROR = 3 NOT_FOUND = 4 REMOTE_ERROR = 5 PARTIAL = 6 class MuseCLIError(Exception): """Base exception for Muse CLI errors.""" def __init__(self, message: str, exit_code: ExitCode = ExitCode.INTERNAL_ERROR) -> None: super().__init__(message) self.exit_code = exit_code class RepoNotFoundError(MuseCLIError): """Raised when the current directory is not a Muse repository.""" def __init__(self, message: str = "Not a Muse repository. Run `muse init`.") -> None: super().__init__(message, exit_code=ExitCode.REPO_NOT_FOUND) #: Canonical public alias matching the name specified. MuseNotARepoError = RepoNotFoundError class UntrustedRepositoryError(PermissionError): """Raised when a ``.muse/`` directory is owned by a different user. This is the Muse equivalent of CVE-2022-24765: an attacker-owned repository in a shared-filesystem location (e.g. ``/tmp``, a Docker bind-mount, a multi-user home directory) could inject malicious configuration or hooks that execute under the victim's UID. Escape hatches (bypass ownership check): - Set ``MUSE_SAFE_DIRS`` environment variable to a colon-separated list of absolute paths that are trusted regardless of ownership. - Add paths via ``muse trust add `` which writes to ``~/.muse/config.toml`` under ``[security] safe_dirs``. Root (uid == 0) bypasses ownership checks entirely since root can already read and write any file regardless of ownership. """ def __init__(self, path: str, owner_uid: int, current_uid: int) -> None: self.repo_path = path self.owner_uid = owner_uid self.current_uid = current_uid message = ( f"Untrusted repository at {path!r}: owned by UID {owner_uid}, " f"but running as UID {current_uid}. " "This could be a security risk (CVE-2022-24765 equivalent). " f"To trust this repository, run: muse trust add {path}" ) super().__init__(message) class HubFingerprintMismatchError(Exception): """Raised when a hub's TLS certificate fingerprint changes unexpectedly. TOFU (Trust On First Use) pinning stores the server's certificate fingerprint on the first connection. If a subsequent connection presents a different fingerprint, this exception is raised to alert the user to a possible man-in-the-middle attack or certificate rotation. To re-pin after a legitimate certificate change, run: muse trust hub-reset """ def __init__(self, hostname: str, stored: str, actual: str) -> None: self.hostname = hostname self.stored_fingerprint = stored self.actual_fingerprint = actual message = ( f"Hub fingerprint mismatch for {hostname!r}!\n" f" Stored: {stored}\n" f" Actual: {actual}\n" "This may indicate a man-in-the-middle attack or a legitimate " "certificate rotation.\n" f"If you expected this change, run: muse trust hub-reset {hostname}" ) super().__init__(message)