gabriel / muse public
local-dev-safety.md markdown
94 lines 5.6 KB
Raw
sha256:133f9bcf57a62ec7ebf0cd71138b54a200b15989aea2fb2a6912497e2926aa8a feat(dev-safety): Phase 1 of #185 — audit tool for ambiguou… Sonnet 5 patch 6 days ago

Local Dev Safety: Isolating muse's Dev Build From Its Own Object Store

Tracking issue: musehub staging #185.

The problem

~/ecosystem/muse is dogfooded: it's the source code we edit and a muse-tracked repo with its own .muse/ object store (currently ~1.0GB) that cannot be corrupted without real cost. ~/ecosystem/musehub has the same shape (~809MB .muse/).

Before this doc existed, the only mitigation was a manual, ad hoc full-directory copy to ~/dev/backups/{muse,musehub} before risky changes — no verification, no rotation, easy to forget.

Phase 1 finding: the resolved muse binary is ambiguous, and the default is the risky one

Running scripts/dev/which_muse.py (this phase's audit tool) against every muse-named binary reachable on this machine found five distinct resolutions, not the "at least four" originally suspected when #185 was filed:

Resolved via Path Editable? Version Source root
which muse (interactive shell) /Users/gabriel/bin/muse/Users/gabriel/ecosystem/muse/.venv/bin/muse yes 0.2.1 /Users/gabriel/ecosystem/muse
homebrew /opt/homebrew/bin/muse yes 0.2.1 /Users/gabriel/ecosystem/muse
installer (getting-started docs) /Users/gabriel/.local/bin/muse~/.local/share/muse/venv/bin/muse no 0.2.0.dev1 ~/.local/share/muse/venv/.../site-packages
pyenv shim /Users/gabriel/.pyenv/shims/muse n/a — bash dispatcher, not directly classifiable delegates to pyenv's active version
pyenv-managed install (what the shim above dispatches to, and also what Python's own shutil.which resolves from this doc's own audit script) /Users/gabriel/.pyenv/versions/3.14.4/bin/muse no 0.2.0rc15 /Users/gabriel/.pyenv/versions/3.14.4/lib/python3.14/site-packages

Three separate versions (0.2.1, 0.2.0.dev1, 0.2.0rc15) are simultaneously installed and reachable depending on how muse is invoked. Worse: even this audit tooling's own shutil.which("muse") (run from a Python subprocess) resolved to a different binary than the interactive shell's which muse — the two disagreed because PATH itself differs across invocation contexts (interactive shell vs. tool/script subprocess). That is direct, reproduced evidence of the exact hazard #185 exists to close: it is not safe to assume "the muse that runs" is a fixed, known thing.

The binary that wins in an interactive terminal (/Users/gabriel/bin/muse) is the editable one — code loaded live from ~/ecosystem/muse's own working tree. So by default, every muse command run in a terminal on this machine — including ones aimed at musehub, agentception, or any other repo via -C — executes code that may be mid-edit, syntactically valid but semantically broken, sourced from the exact repo it can also mutate.

A real bug this audit tooling itself hit

While building the classifier (scripts/dev/which_muse.py), its first implementation had a genuine correctness bug worth recording here because it's the same class of hazard as the ambiguity above: the introspection subprocess inherited the caller's current working directory, and Python's '' (cwd) sys.path entry is searched before PYTHONPATH. Since the tool is naturally run from inside ~/ecosystem/muse (which has a muse/ subpackage sitting at its own root), that cwd entry silently shadowed whatever binary was actually being classified — every call resolved to the real editable checkout regardless of the --target given. Fixed by pinning the introspection subprocess's cwd to a neutral temp directory. Caught by the Phase 1 test suite (tests/test_which_muse_script.py), not by manual inspection — exactly the point of writing tests first.

What Phase 1 delivers

  • scripts/dev/which_muse.py — classifies any muse-like executable as editable (live source) or installed, reporting path, resolved_path, editable, version, source_root. Handles both literal (#!/path/to/python3) and indirect (#!/usr/bin/env [-S] python3) shebangs, and fails with a specific, actionable error for non-Python dispatcher shims (e.g. pyenv's #!/usr/bin/env bash shims) rather than an opaque stack trace.
  • scripts/dev/which-muse.sh — thin PATH-friendly wrapper (which-muse.sh --json).
  • tests/test_which_muse_script.py — 8 tests, all fixture-based (no real pip install needed), covering: editable classification, installed classification, symlink resolution, missing-package error, env-indirect shebangs, non-Python dispatcher shebangs, and the CLI's JSON output shape.

This is observation-only — no PATH changes, no new binaries, no guard rails yet. Those come in Phases 2 and 3 of #185.

What's next

  • Phase 2: split muse (always resolves to a vetted, non-editable install) from a distinctly-named muse-dev (explicit, editable) so there is never ambiguity about which build is running.
  • Phase 3: a guard rail so muse-dev refuses mutating commands against ~/ecosystem/muse or ~/ecosystem/musehub without an explicit MUSE_DEV_ALLOW_CANONICAL=1 override.
  • Phase 4: disposable APFS copy-on-write sandbox clones for actually doing the dev work Phase 3 protects against.
  • Phase 5: two independent automated backup mechanisms (fast local COW snapshots + verified muse bundle archives), both rotated, plus a restore script.
  • Phase 6: wire it together, update agent-guide.md, retire the manual ~/dev/backups full-copy habit once the new pipeline is proven.

See #185 for the full multiphase plan and rationale.

File History 1 commit
sha256:133f9bcf57a62ec7ebf0cd71138b54a200b15989aea2fb2a6912497e2926aa8a feat(dev-safety): Phase 1 of #185 — audit tool for ambiguou… Sonnet 5 patch 6 days ago