"""muse trust — manage trusted repository paths and pinned hub fingerprints. Security model -------------- Muse performs an ownership check on every discovered ``.muse/`` directory (CVE-2022-24765 equivalent). If the ``.muse/`` directory is owned by a different UID, Muse refuses to operate on it and raises :class:`~muse.core.errors.UntrustedRepositoryError`. For shared-filesystem environments (Docker bind-mounts, CI agents, multi-user workstations), specific paths can be added to the trust list so Muse operates normally without re-checking ownership. Trust sources (in evaluation order) ------------------------------------ 1. ``MUSE_SAFE_DIRS`` environment variable — colon-separated absolute paths. Useful for ephemeral CI containers. 2. ``~/.muse/config.toml`` ``[security] safe_dirs`` — persistent trust list. Managed by ``muse trust add`` / ``muse trust remove``. Hub fingerprint pinning (TOFU) ------------------------------- ``muse trust hub-list`` and ``muse trust hub-reset`` manage the TOFU (Trust On First Use) fingerprint store at ``~/.muse/hub_trust.toml``. """ from __future__ import annotations import argparse import sys def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None: """Register the ``muse trust`` namespace and its subcommands.""" trust_parser = subparsers.add_parser( "trust", help="Manage trusted repository paths and pinned hub fingerprints.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) trust_subs = trust_parser.add_subparsers(dest="trust_command", metavar="TRUST_COMMAND") trust_subs.required = True # muse trust add add_p = trust_subs.add_parser( "add", help="Add a path to the trusted directory list (~/.muse/config.toml).", description=( "Add PATH to the [security] safe_dirs list in ~/.muse/config.toml.\n" "Muse will skip the ownership check for this repository.\n\n" "Example:\n" " muse trust add /shared/workspaces/myrepo" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) add_p.add_argument("path", help="Repository path to trust (absolute or relative).") add_p.set_defaults(func=_run_add) # muse trust remove remove_p = trust_subs.add_parser( "remove", help="Remove a path from the trusted directory list.", description=( "Remove PATH from the [security] safe_dirs list in ~/.muse/config.toml." ), formatter_class=argparse.RawDescriptionHelpFormatter, ) remove_p.add_argument("path", help="Repository path to remove from the trust list.") remove_p.set_defaults(func=_run_remove) # muse trust list list_p = trust_subs.add_parser( "list", help="Print all currently trusted paths.", description="Print all paths in the [security] safe_dirs list.", ) list_p.set_defaults(func=_run_list) # muse trust hub-list hub_list_p = trust_subs.add_parser( "hub-list", help="List all pinned hub fingerprints (TOFU store).", description=( "Print all hub fingerprints stored in ~/.muse/hub_trust.toml.\n" "Each entry shows: hostname, SHA-256 fingerprint, first seen, verified count." ), formatter_class=argparse.RawDescriptionHelpFormatter, ) hub_list_p.set_defaults(func=_run_hub_list) # muse trust hub-reset hub_reset_p = trust_subs.add_parser( "hub-reset", help="Remove a pinned hub fingerprint so the next connection re-pins (TOFU reset).", description=( "Remove the stored fingerprint for HOSTNAME from ~/.muse/hub_trust.toml.\n" "The next connection to this hub will re-pin with TOFU.\n\n" "Use this after a legitimate certificate rotation to acknowledge the change." ), formatter_class=argparse.RawDescriptionHelpFormatter, ) hub_reset_p.add_argument("hostname", help="Hub hostname (e.g. 'musehub.ai' or 'localhost:10003').") hub_reset_p.set_defaults(func=_run_hub_reset) trust_parser.set_defaults(func=lambda args: trust_parser.print_help() or sys.exit(0)) def _run_add(args: argparse.Namespace) -> None: """Add a path to the global safe_dirs list.""" from muse.cli.config import add_global_safe_dir import os abs_path = os.path.abspath(args.path) add_global_safe_dir(abs_path) print(f"Trusted path added: {abs_path}") print(f"Muse will skip the ownership check for: {abs_path}") def _run_remove(args: argparse.Namespace) -> None: """Remove a path from the global safe_dirs list.""" from muse.cli.config import remove_global_safe_dir, get_global_safe_dirs import os abs_path = os.path.abspath(args.path) before = get_global_safe_dirs() remove_global_safe_dir(abs_path) after = get_global_safe_dirs() if abs_path in before and abs_path not in after: print(f"Trusted path removed: {abs_path}") else: print(f"Path not in trust list: {abs_path}") def _run_list(args: argparse.Namespace) -> None: """Print all trusted paths.""" from muse.cli.config import get_global_safe_dirs dirs = get_global_safe_dirs() if not dirs: print("No trusted directories configured.") print("Add paths with: muse trust add ") return print("Trusted directories (~/.muse/config.toml):") for d in dirs: print(f" {d}") def _run_hub_list(args: argparse.Namespace) -> None: """Print all pinned hub fingerprints.""" from muse.core.hub_trust import load_hub_trust_store store = load_hub_trust_store() if not store: print("No hub fingerprints pinned.") print("Fingerprints are pinned automatically on first connection.") return print("Pinned hub fingerprints (~/.muse/hub_trust.toml):") for hostname, record in sorted(store.items()): print( f" {hostname}\n" f" fingerprint: {record['fingerprint']}\n" f" first_seen: {record['first_seen']}\n" f" verified_count: {record['verified_count']}" ) def _run_hub_reset(args: argparse.Namespace) -> None: """Remove a hub fingerprint so the next connection re-pins.""" from muse.core.hub_trust import remove_hub_record hostname = args.hostname removed = remove_hub_record(hostname) if removed: print(f"Hub fingerprint reset for: {hostname}") print("The next connection will re-pin with TOFU.") else: print(f"No pinned fingerprint found for: {hostname}")