gabriel / muse public
dev_guard.py python
124 lines 4.8 KB
Raw
sha256:2a8dfcf895b528326eedf00a21babac1a18d90176b856ea1e0fb3027b39be0da feat(dev-safety): Phase 3 of #185 — guard rail blocks mutat… Sonnet 5 patch 2 days ago
1 """Guard rail: refuse mutating commands against a protected canonical repo
2 when running from an editable (live-source) build.
3
4 Phase 3 of musehub staging #185. The hazard: an editable install (e.g. the
5 `muse-dev` entry point created in Phase 2, `pip install -e ~/ecosystem/muse`)
6 loads code straight from a repo it can also mutate. A mid-edit, syntactically
7 valid but semantically broken build can corrupt that repo's own `.muse`
8 object store. This module makes that require an explicit, loud opt-in
9 instead of happening by default.
10
11 Only applies to editable builds — a stable, non-editable `muse` (installed
12 via the official installer venv) has no live-source hazard and is never
13 blocked by this guard, regardless of target.
14
15 Scope: local object-store-mutating commands only. `hub` subcommands go over
16 the network to a remote server and are a different risk category (remote
17 data correctness, not local object-store corruption) — deliberately out of
18 scope here.
19 """
20 from __future__ import annotations
21
22 import argparse
23 import os
24 from pathlib import Path
25
26 DEFAULT_PROTECTED_ROOTS = [
27 str(Path.home() / "ecosystem" / "muse"),
28 str(Path.home() / "ecosystem" / "musehub"),
29 ]
30
31 OVERRIDE_ENV_VAR = "MUSE_DEV_ALLOW_CANONICAL"
32 PROTECTED_ROOTS_ENV_VAR = "MUSE_DEV_PROTECTED_ROOTS"
33
34 # Top-level commands that write to the local object store, refs, or working
35 # tree in a way that's hard to reverse if the code producing them is broken.
36 _MUTATING_TOP_LEVEL_COMMANDS = {
37 "commit", "merge", "push", "pull", "reset", "rm", "revert",
38 "cherry-pick", "rebase", "restore", "apply", "apply-patch",
39 "unpack-objects", "update-ref", "prune", "gc", "clean",
40 "checkout-symbol", "mv", "commit-tree", "symbolic-ref", "bundle",
41 "sparse-checkout",
42 }
43
44 # `code <subcommand>` is a mixed namespace — most of it is pure read/analysis
45 # (grep, impact, hotspots, ...); only these actually mutate tracked content.
46 _MUTATING_CODE_SUBCOMMANDS = {
47 "add", "reset", "patch", "rename", "migrate", "semantic-cherry-pick",
48 }
49
50
51 class GuardBlocked(SystemExit):
52 """Raised (as a SystemExit subclass) when a mutating command is refused."""
53
54
55 def is_editable_build() -> bool:
56 """True if this process is running muse's code straight from a source
57 checkout rather than a venv's copied ``site-packages`` tree."""
58 import muse
59 return "site-packages" not in str(Path(muse.__file__).resolve().parent.parent)
60
61
62 def _protected_roots_from_env() -> list[str]:
63 raw = os.environ.get(PROTECTED_ROOTS_ENV_VAR)
64 if raw:
65 return [p for p in raw.split(":") if p]
66 return DEFAULT_PROTECTED_ROOTS
67
68
69 def is_protected_root(path: str, *, protected_roots: list[str] | None = None) -> bool:
70 """True if ``path`` is, or is nested inside, any protected canonical repo root."""
71 roots = protected_roots if protected_roots is not None else _protected_roots_from_env()
72 resolved = Path(path).resolve()
73 for root in roots:
74 root_resolved = Path(root).resolve()
75 if resolved == root_resolved or root_resolved in resolved.parents:
76 return True
77 return False
78
79
80 def classify_is_mutating(args: argparse.Namespace) -> bool:
81 """True if the parsed CLI args represent a local-object-store-mutating command."""
82 command = getattr(args, "command", None)
83 if command == "code":
84 return getattr(args, "code_command", None) in _MUTATING_CODE_SUBCOMMANDS
85 return command in _MUTATING_TOP_LEVEL_COMMANDS
86
87
88 def check_guard(
89 args: argparse.Namespace,
90 *,
91 cwd: str | None = None,
92 protected_roots: list[str] | None = None,
93 is_editable: bool | None = None,
94 ) -> None:
95 """Raise :class:`GuardBlocked` if this call should be refused.
96
97 All keyword params default to the real live values (current process's
98 editable-ness, real cwd, env-configured protected roots) — tests pass
99 them explicitly to avoid ever touching the real canonical repos.
100 """
101 editable = is_editable_build() if is_editable is None else is_editable
102 if not editable:
103 return
104 if not classify_is_mutating(args):
105 return
106
107 effective_cwd = os.getcwd() if cwd is None else cwd
108 if not is_protected_root(effective_cwd, protected_roots=protected_roots):
109 return
110
111 if os.environ.get(OVERRIDE_ENV_VAR) == "1":
112 return
113
114 command = getattr(args, "command", "?")
115 sub = getattr(args, "code_command", None)
116 full_command = f"{command} {sub}" if sub else command
117 raise GuardBlocked(
118 f"❌ Refusing to run mutating command `muse {full_command}` from an editable "
119 f"build against a protected canonical repo ({effective_cwd}).\n"
120 f" This is muse-dev's guard rail (musehub staging #185) — an editable "
121 f"install can corrupt the object store of the very repo it was built from.\n"
122 f" Set {OVERRIDE_ENV_VAR}=1 to override, only if you're certain this is "
123 f"intentional."
124 )
File History 1 commit
sha256:2a8dfcf895b528326eedf00a21babac1a18d90176b856ea1e0fb3027b39be0da feat(dev-safety): Phase 3 of #185 — guard rail blocks mutat… Sonnet 5 patch 2 days ago