diff --git a/README.md b/README.md index d76f8c0..7e8705d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ SandVault (`sv`) manages a limited user account to sandbox shell commands and AI

-- **AI ready** - Includes Claude Code, OpenAI Codex, OpenCode, Google Gemini, pi +- **AI ready** - Includes Claude Code, OpenAI Codex, OpenCode, Google Gemini, pi, Muse Code - **Web and iOS automation** - sandbox access to Chrome / Lightpanda and iOS Simulator - **Fast context switching** - No VM overhead; instant user switching - **Passwordless** - switch accounts without a prompt (after setup) @@ -94,6 +94,10 @@ Install via git: # shortcut: sv p sv pi +# Run Muse Code in the sandbox +# shortcut: sv m + sv muse + # Run command shell in the sandbox # shortcut: sv s sv shell @@ -182,6 +186,7 @@ By default, SandVault installs AI tools via Homebrew on the host side. With `--n - **OpenCode** — installed via `curl -fsSL https://opencode.ai/install | bash` - **Gemini** — installed via `npm install -g @google/gemini-cli` - **pi** — installed via `npm install -g @earendil-works/pi-coding-agent` +- **Muse Code** — installed via `curl -fsSL https://dev.meta.ai/install.sh | bash` Tools are installed on first run and reused on subsequent runs. @@ -195,6 +200,7 @@ sv -N codex sv -N opencode sv -N gemini sv -N pi +sv -N muse ``` To make native install the default, set `SANDVAULT_ARGS`: @@ -258,7 +264,7 @@ Explicit command-line arguments are appended after `SANDVAULT_ARGS`, so they are ## `agentsview` Integration -[`agentsview`](https://github.com/badlogic/agentsview) is a dashboard for AI coding agents (Claude Code, Codex, OpenCode, Gemini, pi). It shows session history, search, and cost tracking. If you have agentsview installed on the host, `sv-agentsview-setup` mirrors sandbox session data so that it appears next to your host-side sessions. +[`agentsview`](https://github.com/badlogic/agentsview) is a dashboard for AI coding agents (Claude Code, Codex, OpenCode, Gemini, pi). It shows session history, search, and cost tracking. If you have agentsview installed on the host, `sv-agentsview-setup` mirrors sandbox session data so that it appears next to your host-side sessions. Muse Code is not included: agentsview has no parser for its session format yet. ```bash # Detect agentsview, prompt to opt in, and configure @@ -408,6 +414,11 @@ Next time you run sandvault, your files will be copied to the sandvault user hom SandVault supports a headless browser for automation from within the sandbox. The browser runs on the host side and the sandbox connects to it via the Chrome DevTools Protocol (CDP) over localhost. Two backends are supported: **Chrome** (default) and **Lightpanda**. +> **Muse Code:** `sv --browser muse` starts the browser, but muse is not told the +> endpoint exists — it is the one agent with no way to inject a system prompt. The +> `SV_BROWSER_ENDPOINT` variable below is still set in its environment, so mention +> it in your prompt or the project's `AGENTS.md` to make muse aware of it. + ### Usage ```bash @@ -447,6 +458,11 @@ See also [`./tests/browser/*.js`](./tests/browser) for examples of using Playwri SandVault can expose the iOS Simulator to sandboxed AI agents for iOS app testing. The simulator runs on the host (it is a GUI app and cannot run inside the sandbox), and an HTTP bridge on localhost translates sandbox-side requests into `xcrun simctl` and [`iosef`](https://github.com/riwsky/iosef) invocations. +> **Muse Code:** `sv --ios muse` boots the simulator, but muse is not told the +> bridge exists — it is the one agent with no way to inject a system prompt. The +> `SV_IOS_SIMULATOR_ENDPOINT` variable below is still set in its environment, so +> mention it in your prompt or the project's `AGENTS.md`. + ### Usage ```bash @@ -528,6 +544,7 @@ After exploring Docker containers, Podman, sandbox-exec, and virtualization, I n - Runs OpenCode with `OPENCODE_PERMISSION='{"*":"allow"}'` - Runs Google Gemini with `--yolo` - Runs pi with `--approve` to trust project-local files (pi needs no permission bypass) +- Runs Muse Code with `--yolo`, which disables both its approval prompts and its own sandbox — sandvault is already the sandbox, and muse's nested one defaults to proxy-only network and workspace-only writes - Automates Chrome for web testing (via Chrome DevTools Protocol) - Automated iOS Simulator for app testing (via `xcrun simctl`, and `iosef`) - Maintains a clean separation between trusted and untrusted code diff --git a/guest/home/bin/muse b/guest/home/bin/muse new file mode 100755 index 0000000..a575a25 --- /dev/null +++ b/guest/home/bin/muse @@ -0,0 +1,47 @@ +#!/bin/bash +set -Eeuo pipefail +trap 'echo >&2 "${BASH_SOURCE[0]}: line $LINENO: $BASH_COMMAND: exitcode $?"' ERR + +unset MUSE +if [[ "${SV_NATIVE_INSTALL:-}" == "true" ]]; then + if [[ ! -x "$HOME/.local/bin/muse" ]]; then + echo >&2 "Installing Muse Code natively..." + # MUSE_INSTALL_DIR is the installer's default, set explicitly so the + # binary lands where the line below looks for it regardless of the + # installer's future defaults. MUSE_NO_MODIFY_PATH stops the installer + # appending "export PATH=..." to .zshrc/.zprofile: sandvault owns those + # files and rewrites them from guest/home on every build, so the edit is + # both lost and noise. ~/.local/bin is already on PATH via .zprofile. + curl -fsSL https://dev.meta.ai/install.sh \ + | MUSE_INSTALL_DIR="$HOME/.local/bin" MUSE_NO_MODIFY_PATH=1 bash + fi + MUSE="$HOME/.local/bin/muse" +elif command -v brew &>/dev/null ; then + HOMEBREW_PREFIX="${HOMEBREW_PREFIX:-"$(brew --prefix)"}" + if [[ -x "$HOMEBREW_PREFIX/bin/muse" ]]; then + # Homebrew install (the "muse-code" cask ships a "muse" binary) + MUSE="$HOMEBREW_PREFIX/bin/muse" + fi +fi + +if [[ -z "${MUSE:-}" || ! -x "$MUSE" ]]; then + echo >&2 "ERROR: muse is not installed" + exit 1 +fi + +# No sv-tool-prompts.sh source: muse 1.0.2 has no way to inject a session +# prompt -- no --append-system-prompt, no instructions-file option, no +# extra-context directory. It reads instructions only from rules files +# (AGENTS.md and friends), which would mean writing into the user's repo or +# another agent's config and outliving the session. So `sv -b/-i muse` set +# SV_BROWSER_ENDPOINT / SV_IOS_SIMULATOR_ENDPOINT but never announce them; +# the README says so too. Revisit if muse gains a system-prompt flag. + +# --yolo is muse's own "approval and sandbox off" switch, the direct analogue of +# codex's --dangerously-bypass-approvals-and-sandbox: muse ships a Seatbelt +# sandbox and staged approval prompts, both ON by default. Sandvault is already +# the sandbox, so muse's nested one adds no protection while defaulting to +# proxy-only network and workspace-only writes. --yolo also trusts the workspace +# for the run, which is what loads its project-local skills and rules. +echo >&2 "$USER: running muse --yolo" +exec "$MUSE" --yolo "$@" diff --git a/helpers/agentsview-paths.sh b/helpers/agentsview-paths.sh index 57450a6..101dba1 100644 --- a/helpers/agentsview-paths.sh +++ b/helpers/agentsview-paths.sh @@ -9,6 +9,16 @@ AGENTSVIEW_AGENTS=(claude codex opencode gemini pi) +# muse (Muse Code) is deliberately absent. agentsview 0.42.0 has no muse parser +# and no muse config key -- its nearest entry, commandcode_project_dirs +# (~/.commandcode/projects), is a different tool; muse keeps sessions under +# ~/.local/share/muse/sessions. Mirroring that directory would copy files +# agentsview cannot read. Add muse here (SUBDIR/DEFAULT/TOMLKEY, the +# VALID_KEYS and DEFAULT_SUBPATHS tables in agentsview-config.py, and the +# fixtures in scripts/test-agentsview-resync.sh) once agentsview ships one. +# Existing opt-in users then pick it up the documented way: delete +# $SV_PRIVATE_DIR/setup/agentsview-export.state and re-run sv-agentsview-setup. + # Sandbox-side path under /Users/sandvault-$USER/ AGENTSVIEW_SUBDIR_claude=".claude/projects" AGENTSVIEW_SUBDIR_codex=".codex/sessions" diff --git a/scripts/tests b/scripts/tests index a8b283a..98246d1 100755 --- a/scripts/tests +++ b/scripts/tests @@ -722,6 +722,136 @@ EOF } queue_test test_pi_wrapper_no_command_v_loop + # muse listing, alongside the opencode and pi help tests. + test_help_lists_muse() { + local output + output=$(sv_cmd --help 2>&1) + if [[ "$output" == *"m, muse"* ]]; then + pass "--help lists muse" + else + fail "--help lists muse" "muse command line" "$output" + fi + } + queue_test test_help_lists_muse + + # Muse ships its own Seatbelt sandbox and staged approval prompts, both ON + # by default; --yolo turns both off and trusts the workspace, the direct + # analogue of codex's --dangerously-bypass-approvals-and-sandbox. Anchor it + # to the exec line, like the pi test: --yolo also appears in a comment and + # the startup banner, so a whole-file grep would stay green after it was + # dropped from the exec -- exactly the drift this guards. + test_muse_wrapper_uses_yolo() { + local wrapper="$SCRIPT_DIR/../guest/home/bin/muse" + if [[ ! -x "$wrapper" ]]; then + fail "muse wrapper uses --yolo" "executable wrapper" "$wrapper" + return + fi + if grep -Eq '^exec .*--yolo' "$wrapper"; then + pass "muse wrapper uses --yolo" + else + fail "muse wrapper uses --yolo" \ + "--yolo on the exec line" "$wrapper" + fi + } + queue_test test_muse_wrapper_uses_yolo + + # Guard the dispatch mapping, as for pi: a typo in (or removal of) the + # m|muse) case arm would pass the wrapper static tests but break 'sv muse'. + test_sv_dispatches_muse() { + if grep -Eq '^[[:space:]]+m[|]muse\)' "$SV_BASE" \ + && grep -A2 -E '^[[:space:]]+m[|]muse\)' "$SV_BASE" | grep -q 'COMMAND=muse'; then + pass "sv dispatches m|muse -> COMMAND=muse" + else + fail "sv dispatches m|muse -> COMMAND=muse" \ + "an m|muse) case arm setting COMMAND=muse" \ + "missing or broken dispatch arm in $SV_BASE" + fi + } + queue_test test_sv_dispatches_muse + + # Same error class as the pi formula test: muse installs from the + # homebrew/cask "muse-code" token, whose name differs from its "muse" + # binary. Passing only the token would look for a "muse-code" binary that + # does not exist, reinstalling on every run. + test_sv_installs_muse_via_brew_cask() { + if grep -F -- 'ensure_brew_tool "muse-code" "muse"' "$SV_BASE" &>/dev/null; then + pass "sv installs muse via the muse-code cask" + else + fail "sv installs muse via the muse-code cask" \ + 'ensure_brew_tool "muse-code" "muse"' \ + "missing or broken install arm in $SV_BASE" + fi + } + queue_test test_sv_installs_muse_via_brew_cask + + # The other install path. Muse is not an npm package: its installer drops a + # self-updating launcher into MUSE_INSTALL_DIR and would append PATH lines + # to the shell rc files that sandvault owns and rewrites from guest/home on + # every build, so MUSE_NO_MODIFY_PATH must stay set. Both surface only at + # runtime under 'sv -N muse', which the suite never exercises. + test_muse_wrapper_native_install() { + local wrapper="$SCRIPT_DIR/../guest/home/bin/muse" + if [[ ! -x "$wrapper" ]]; then + fail "muse wrapper native-installs from dev.meta.ai" \ + "executable wrapper" "$wrapper" + return + fi + local code + code=$(grep -vE '^[[:space:]]*#' "$wrapper" | sed 's/[[:space:]]#.*$//') + if ! grep -F -- 'https://dev.meta.ai/install.sh' <<< "$code" &>/dev/null; then + fail "muse wrapper native-installs from dev.meta.ai" \ + 'curl -fsSL https://dev.meta.ai/install.sh' "$wrapper" + elif ! grep -F -- 'MUSE_INSTALL_DIR="$HOME/.local/bin"' <<< "$code" &>/dev/null; then + # Not redundant with the installer's default. This pins the install + # dir to the one path the wrapper then execs, so an upstream default + # change cannot separate them. Were they to diverge, the + # [[ ! -x "$HOME/.local/bin/muse" ]] guard would never become false: + # every 'sv -N muse' would re-download ~230MB, re-run the installer, + # and still exit "ERROR: muse is not installed". + fail "muse wrapper native-installs from dev.meta.ai" \ + 'MUSE_INSTALL_DIR="$HOME/.local/bin" pinned to the exec path' \ + "$wrapper" + elif ! grep -F -- 'MUSE_NO_MODIFY_PATH=1' <<< "$code" &>/dev/null; then + fail "muse wrapper native-installs from dev.meta.ai" \ + "MUSE_NO_MODIFY_PATH=1 so the installer leaves .zshrc alone" \ + "$wrapper" + else + pass "muse wrapper native-installs from dev.meta.ai" + fi + } + queue_test test_muse_wrapper_native_install + + # muse is deliberately NOT in AGENTSVIEW_AGENTS: agentsview 0.42.0 has no + # muse parser or config key, so a mirror would copy files it cannot read. + # Assert the omission is still deliberate rather than half-done -- a muse + # entry with no matching key in agentsview-config.py's VALID_KEYS would + # abort sv-agentsview-setup for every agent, not just muse. + test_agentsview_omits_muse() { + local paths="$SCRIPT_DIR/../helpers/agentsview-paths.sh" + if [[ ! -r "$paths" ]]; then + fail "agentsview omits muse" "readable agentsview-paths.sh" "$paths" + return + fi + # shellcheck source=/dev/null + source "$paths" + local found=false agent + for agent in "${AGENTSVIEW_AGENTS[@]}"; do + [[ "$agent" == "muse" ]] && found=true + done + if [[ "$found" == "true" ]]; then + fail "agentsview omits muse" \ + "muse absent from AGENTSVIEW_AGENTS until agentsview supports it" \ + "muse is listed; add its key to VALID_KEYS/DEFAULT_SUBPATHS in agentsview-config.py and the scripts/test-agentsview-resync.sh fixtures too" + elif ! grep -q 'muse' "$paths"; then + fail "agentsview omits muse" \ + "a comment recording why muse is omitted" \ + "no muse note in $paths -- the omission looks accidental" + else + pass "agentsview omits muse" + fi + } + queue_test test_agentsview_omits_muse + # Unknown option test_unknown_option() { local output @@ -935,7 +1065,7 @@ EOF fi local failed=false local link - for link in claude codex opencode gemini pi; do + for link in claude codex opencode gemini pi muse; do if [[ -L "$brew_prefix/bin/$link" ]]; then local link_perms link_perms=$(/usr/bin/stat -f "%Lp" "$brew_prefix/bin/$link") diff --git a/sv b/sv index e4dfc62..f506486 100755 --- a/sv +++ b/sv @@ -470,6 +470,12 @@ install_deps () { pi) ensure_brew_tool "pi-coding-agent" "pi" ;; + muse) + # The "muse-code" cask ships a "muse" binary; names differ, so + # both arguments are required. Casks are quarantined on + # download, which ensure_brew_tool's warm-up step clears. + ensure_brew_tool "muse-code" "muse" + ;; *) # No tool installation needed for other commands ;; @@ -1006,11 +1012,12 @@ show_help() { echo " o, opencode [PATH] Open OpenCode in sandvault" echo " g, gemini [PATH] Open Google Gemini in sandvault" echo " p, pi [PATH] Open pi in sandvault" + echo " m, muse [PATH] Open Muse Code in sandvault" echo " s, shell [PATH] Open shell in sandvault" echo " b, build Build sandvault" echo " u, uninstall Remove sandvault; keep shared files" echo "" - echo "Arguments after -- are passed to the command (claude, codex, opencode, gemini, pi, shell)" + echo "Arguments after -- are passed to the command (claude, codex, opencode, gemini, pi, muse, shell)" echo "" echo "Environment:" echo " SANDVAULT_ARGS Default arguments (prepended to command line)" @@ -1142,6 +1149,10 @@ case "${1:-}" in COMMAND=pi INITIAL_DIR="${2:-}" ;; + m|muse) + COMMAND=muse + INITIAL_DIR="${2:-}" + ;; s|shell) COMMAND= INITIAL_DIR="${2:-}" @@ -1828,7 +1839,7 @@ if [[ "$FIX_PERMISSIONS" == "true" ]]; then # Fix homebrew symlinks for any installed tools # shellcheck disable=SC2310 # brew_shellenv intentionally used in condition if brew_shellenv 2>/dev/null; then - for tool_cli in claude codex opencode gemini pi; do + for tool_cli in claude codex opencode gemini pi muse; do brew_link="$(brew --prefix)/bin/$tool_cli" if [[ -L "$brew_link" ]]; then link_perms=$(/usr/bin/stat -f "%Lp" "$brew_link")