gabriel / muse public
feat patch dev-safety feat/185-phase4-sandbox-tooling #1 / 1
AI Agent gabriel · 3 days ago · Sep 10, 2026 · Diff

feat(dev-safety): Phase 4 of #185 — disposable sandbox tooling for muse-dev

Adds scripts/dev/sandbox.py + sandbox-refresh.sh + sandbox-run.sh: gives muse-dev's guard rail (Phase 3) somewhere safe to actually be used for real dev work. sandbox-refresh.sh <repo> clones a canonical repo's working tree + .muse/ into a timestamped, disposable copy via APFS copy-on-write (`cp -c -R`) — near-instant, near-zero extra disk until the copy diverges. sandbox-run.sh <repo> -- <args> wraps `muse-dev -C <current-sandbox> <args>`. A `current` symlink tracks the latest refresh; old timestamped clones beyond --keep (default 5) are pruned automatically.

8 tests, written first, all against tmp_path fixtures — never the real ~/ecosystem/muse (matching test_dev_guard.py's discipline). Covers: branch heads match canonical exactly after refresh, the current symlink always points at the latest, rotation keeps only the newest N, a corruption introduced in the sandbox never touches canonical, editing canonical after a refresh doesn't retroactively affect an already-existing sandbox (proving cp -c produced a genuinely independent copy, not a shared/linked view), unknown repo names fail clearly, and sandbox-run.sh correctly wraps the real muse-dev binary (skipped gracefully if muse-dev isn't installed).

Hit and fixed a real argparse gotcha along the way: nargs=REMAINDER swallows earlier flags too eagerly when they appear before the remainder positional in argv, regardless of `--` placement. sandbox.py now splits the muse-dev tail args on the first literal `--` manually instead of relying on REMAINDER.

Manually verified end-to-end against the real ~/ecosystem/muse: refreshed a real sandbox, ran `muse-dev status --json` against it and got identical untracked-file state to canonical, corrupted an object inside the sandbox only, and confirmed via `muse verify-object --all --json` that canonical was completely unaffected throughout.

Unrelated discovery during that manual verification, filed separately as musehub staging #188 (not blocking this phase): canonical's object store already has ~3,010 objects that fail hash verification, independent of anything done here — `muse verify`'s branch-reachability walk reports everything reachable is clean, and the bad objects appear to be a subset of ~9,363 already-unreachable objects, most likely orphaned leftovers from a historical `muse code migrate --force-resign` pass. Needs its own investigation before any `muse gc`.

sha256:f20267508f836150a1df05f57eaba390a49dae7412c1579a2c26beb3dfe94b50 sha
+42 symbols
4 changed · 1168 in snapshot files
sha256:85ba41f914ea79a0e24594200c9cdc98cf0d2e557cc6563f5510084ed5dd517e snapshot
+42
symbols added
4
files changed
1168
files in snapshot
0
dead code introduced
Semantic Changes 42 symbols
~ scripts/dev/sandbox.py .py 20 symbols added
+ DEFAULT_CANONICAL_ROOTS variable variable DEFAULT_CANONICAL_ROOTS L27–30
+ DEFAULT_KEEP variable variable DEFAULT_KEEP L32–32
+ DEFAULT_SANDBOX_BASE variable variable DEFAULT_SANDBOX_BASE L31–31
+ SandboxNotFoundError class class SandboxNotFoundError L35–36
+ _cmd_refresh function function _cmd_refresh L105–119
+ _cmd_run function function _cmd_run L122–138
+ _prune_old_sandboxes function function _prune_old_sandboxes L86–92
+ _sandbox_dir function function _sandbox_dir L39–40
+ Path import import Path L25–25
+ annotations import import annotations L17–17
+ argparse import import argparse L19–19
+ datetime import import datetime L24–24
+ json import import json L20–20
+ shutil import import shutil L21–21
+ subprocess import import subprocess L22–22
+ sys import import sys L23–23
+ timezone import import timezone L24–24
+ main function function main L141–171
+ refresh_sandbox function function refresh_sandbox L43–83
+ resolve_current_sandbox function function resolve_current_sandbox L95–102
~ tests/test_sandbox.py .py 22 symbols added
+ TestRefreshSandbox class class TestRefreshSandbox L34–101
+ test_corruption_in_sandbox_never_touches_canonical method method test_corruption_in_sandbox_never_touches_canonical L74–85
+ test_current_symlink_points_at_latest_refresh method method test_current_symlink_points_at_latest_refresh L45–55
+ test_editing_canonical_after_refresh_does_not_affect_existing_sandbox method method test_editing_canonical_after_refresh_does_not_affect_existing_sandbox L87–97
+ test_rotation_keeps_only_newest_n method method test_rotation_keeps_only_newest_n L57–72
+ test_sandbox_branch_heads_match_canonical_exactly method method test_sandbox_branch_heads_match_canonical_exactly L35–43
+ test_unknown_repo_name_raises_clear_error method method test_unknown_repo_name_raises_clear_error L99–101
+ TestResolveCurrentSandbox class class TestResolveCurrentSandbox L104–107
+ test_raises_clear_error_when_never_refreshed method method test_raises_clear_error_when_never_refreshed L105–107
+ TestSandboxRunScript class class TestSandboxRunScript L110–142
+ _muse_dev_path method method _muse_dev_path L111–113
+ test_sandbox_run_invokes_muse_dev_against_current_sandbox method method test_sandbox_run_invokes_muse_dev_against_current_sandbox L115–142
+ _make_fake_repo function function _make_fake_repo L20–26
+ _read_branch_heads function function _read_branch_heads L29–31
+ Path import import Path L12–12
+ SandboxNotFoundError import import SandboxNotFoundError L17–17
+ os import import os L9–9
+ pytest import import pytest L14–14
+ refresh_sandbox import import refresh_sandbox L17–17
+ resolve_current_sandbox import import resolve_current_sandbox L17–17
+ subprocess import import subprocess L10–10
+ sys import import sys L11–11
Files Changed
+4
1168 in snapshot
← Older Oldest on feat/185-phase4-sandbox-tooling
All commits
Newer → Latest on feat/185-phase4-sandbox-tooling

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:f20267508f836150a1df05f57eaba390a49dae7412c1579a2c26beb3dfe94b50 --body "your comment"