Imported from papercomputeco/masterblaster (
AGENTS.md). Install upstream withnpx skills add papercomputeco/masterblaster. Copyright stays with the author.
Agents
Guidelines for AI coding agents working on the Masterblaster codebase.
Project overview
Masterblaster (mb) is a Go CLI and daemon for managing StereOS-based
AI agent sandbox VMs. It targets aarch64 guests using HVF acceleration on
Apple Silicon Macs with two backends: QEMU and Apple Virtualization.framework.
The tool communicates with stereosd inside StereOS guests over vsock for
secret injection, shared directory mounting, health monitoring, and graceful
shutdown. Agent harnesses (Claude Code, Claude Code, Gemini CLI) are managed by
agentd inside the guest.
See SPEC.md for the full RFC specification.
Build and test
make build # Produces ./build/mb binary
make check # Runs go tests in Dagger
make clean # Removes the build/ directory
make fmt # Formats code
make vet # Runs go vet
Go 1.25+ is required. The Nix flake (flake.nix) provides a reproducible dev
environment with QEMU, Go, and build tools. Use nix develop or direnv.
Architecture
The codebase follows the daemon + CLI client + vmhost child process pattern:
mb CLI --[JSON-RPC over $config-dir/mb.sock]--> mb daemon (mb serve)
daemon --[JSON-RPC over vmhost.sock]---------> mb vmhost (one per VM)
vmhost --[QMP unix socket]----------------> QEMU process (child of vmhost)
vmhost --[in-process]---------------------> Apple Virt VM (Vz framework)
vmhost --[vsock/tcp]----------------------> stereosd (guest)
stereosd --[tmpfs/unix socket]----------> agentd (guest)
Each VM gets a dedicated vmhost child process (mb vmhost --name <name> --backend <qemu|applevirt>) that holds the hypervisor handle and exposes a
control socket (vmhost.sock). The daemon spawns vmhost processes, monitors
their health via PID liveness checks, and routes CLI requests to them. This
architecture enables crash isolation (one VM crashing doesn't affect others),
daemon restart survival (vmhost processes outlive the daemon), and mixed
backends (QEMU and Apple Virt VMs running simultaneously).
The default config directory is $XDG_CONFIG_HOME/mb (falls back to
~/.config/mb). It can be overridden via --config-dir flag, MB_CONFIG_DIR
env var, or config-dir in config.toml. Precedence: flag > env > config
file > default.
Key packages
-
main.go-- Thin shim entrypoint. Creates the root command (NewMbCmd()), registers persistent flags (--verbose,--config-dir), and wires in all subcommands. APersistentPreRunEhook callsmbconfig.Init()to initialize Viper before any subcommand runs. -
pkg/mbconfig/-- Centralizes config directory resolution using Viper.Init(cmd)binds CLI flags, sets theMB_env prefix (soMB_CONFIG_DIRandMB_VERBOSEwork), applies XDG defaults, and reads an optionalconfig.tomlfrom the resolved config dir. ExposesConfigDir()andVerbose()for use by subcommands. -
cmd/<name>/-- Each subcommand gets its own package directory following theNew<Name>Cmd()factory pattern. Packages:serve,init,up,down,status,destroy,ssh,list,mixtapes,vmhost. Commands are thin wrappers that delegate to the daemon viapkg/client/. -
cmd/vmhost/-- Hiddenmb vmhostsubcommand. The daemon spawns one vmhost process per VM; each holds the hypervisor handle and exposes a control socket.vmhost.godefinesNewVMHostCmd(), therunVMHost()entrypoint, theqemuControlleradapter (implementsvmhost.VMController), and thebootVM()dispatcher. Platform-specific files providebootAppleVirt()andgetPlatformConfig():platform_darwin_arm64.go-- Apple Virt controller + QEMU HVF config.platform_linux.go-- QEMU KVM config.platform_darwin_amd64.go-- Stub (unsupported).platform_other.go-- Stub (unsupported).
-
pkg/config/-- jcard.toml config parsing, validation, and defaults.config.godefinesJcardConfigandLoad().expand.gohandles${ENV_VAR}expansion.marshal.goprovides TOML serialization and the default jcard.toml template. Always useLoad()to get a validated config. -
pkg/daemon/-- The long-lived Masterblaster daemon.daemon.godefines theDaemonstruct with async.RWMutex-protected VM map and unix socket listener. Each VM is tracked as amanagedVMstruct containing the vmhost client connection. The daemon no longer holds aBackenddirectly; it spawns vmhost child processes and delegates all VM operations to them viapkg/vmhost.Client.rpc.godefines the JSON-RPC wire format (Request,Response,SandboxInfo). The daemon manages$config-dir/mb.sockfor CLI communication and$config-dir/daemon.pidfor liveness. -
pkg/daemon/client/-- Thin JSON-RPC client for CLI commands to talk to the daemon. ProvidesUp(),Down(),Status(),Destroy(),List(). Also providesEnsureDaemon(baseDir)which auto-starts the daemon if not running (fork + setsid + poll). All CLI commands (up,status,list,ssh,down,destroy) callEnsureDaemonautomatically so the daemon can rediscover running vmhost processes that may have survived a daemon restart. -
pkg/vm/-- VM lifecycle management.backend.godefines theBackendinterface (Up,Start,Down,ForceDown,Destroy,Status,List,LoadInstance).platform.godefinesQEMUPlatformConfig-- platform-specific settings (accelerator, binary, EFI paths, control plane mode, vsock device, direct kernel boot, disk AIO/cache) injected into the QEMU backend at construction time.qemu.gois the QEMU backend. It usesQEMUPlatformConfigto build QEMU args with the correct accelerator (hvf/kvm), binary, and EFI firmware paths. QEMU runs as a child process of vmhost (no-daemonize). Post-boot it connects to stereosd via the platform's control plane transport to inject secrets and mount directories. ExposesBoot()andWaitQEMU()methods for vmhost use.applevirt.gois the Apple Virtualization.framework backend (darwin/arm64 only, usesgithub.com/Code-Hex/vz/v3). VMs run in-process with an in-memorylivemap. ExposesBoot()andWaitVM()methods for vmhost use.qmp.gois a minimal QMP client for QEMU process control.image.gohandles mixtape resolution, qcow2 overlay creation, and raw image copying/resizing.state.gopersists VM metadata asstate.json.types.godefinesState,Instance, and path helpers (includingVMHostPIDPath(),VMHostSocketPath(),VMHostLogPath()).prepare.gocontains daemon-side disk preparation functions run before spawning vmhost:PrepareQEMUDisk(),LoadInstanceFromDisk(),LoadStateFromDisk(),CleanupVMDir(),ResolveBackend().prepare_darwin_arm64.go--PrepareAppleVirtDisk()implementation.prepare_other.go--PrepareAppleVirtDisk()stub (unsupported).backend_darwin_arm64.go--NewPlatformBackend()for macOS/Apple Silicon. Configures QEMU withaccel=hvfandControlPlaneMode: "tcp"(no native vsock on macOS/HVF).backend_linux.go--NewPlatformBackend()for Linux. Configures QEMU withaccel=kvmandControlPlaneMode: "vsock"(nativevhost-vsock-pci).backend_other.go-- Returns an error on unsupported platforms.
-
pkg/vmhost/-- Control protocol between the daemon and vmhost processes.protocol.godefinesRequest/Responsewire types and method constants (status,stop,force_stop,info).server.godefinesVMControllerinterface (implemented by backend adapters incmd/vmhost/), andServerwhich listens onvmhost.sock, dispatches JSON-RPC requests, and monitors VM exit viacontroller.Wait().client.goprovidesClientused by the daemon to talk to vmhost processes:Status(),Stop(),ForceStop(),Info(),IsAlive().
-
pkg/vsock/-- Host-side client for communicating with stereosd.transport.godefines theTransportinterface, abstracting the connection mechanism.TCPTransportis the current implementation (used on macOS/HVF).VsockTransport(AF_VSOCK, for Linux/KVM) is planned. Used by vmhost internally.client.goprovidesConnect(transport, timeout)and message methods:Ping(),InjectSecret(),Mount(),Shutdown(),GetHealth(),WaitForReady().protocol.godefines the ndjson wire format and message types.
-
pkg/mixtapes/-- Manages local mixtape images in$config-dir/mixtapes/.List()scans available mixtapes,Pull()is the OCI pull placeholder (to be implemented withoras.land/oras-go/v2). -
pkg/ssh/-- SSH connectivity.connect.gousessyscall.Execto replace the Go process with OpenSSH.wait.gopolls TCP until sshd responds. -
pkg/ui/-- Terminal output helpers. Colored status/success/warn/error messages and an animated progress spinner. All output goes to stderr.
Design principles
-
Daemon architecture. The daemon (
mb serve) owns all VM lifecycle. CLI commands are thin RPC clients. The daemon can auto-start viamb upif not running (fork + setsid).mb upis idempotent: if the sandbox is already running it's a no-op, if stopped it re-boots the existing disk. -
Backend interface is the key abstraction. All VM operations go through
vm.Backend. QEMU is the only implementation today. Platform-specificNewPlatformBackend()with build tags enables future backends (Apple Virt framework, KVM/libvirt). -
Vmhost child process pattern. Each VM gets a dedicated
mb vmhostchild process that holds the hypervisor handle and exposes a control socket (vmhost.sock). The daemon spawns vmhost processes and routes CLI requests to them viapkg/vmhost.Client. This provides crash isolation, daemon restart survival, and mixed backend support. -
Vsock for guest control plane. stereosd inside the guest is the bridge. Secrets are injected over vsock (never baked into images). Shared directories are mounted via vsock mount commands. Shutdown is coordinated through vsock.
-
jcard.toml is the config format. Defines mixtape, resources, network (with port forwards and egress allowlists), shared directories, secrets, and agent configuration. The
[agent]section is passed through to agentd. -
SSH uses process replacement.
mb sshcallssyscall.Execfor correct terminal handling. Do not change to a Go SSH library. -
Config defaults are generous. Most fields in jcard.toml are optional.
applyDefaults()fills in sensible values (2 CPUs, 4GiB RAM, 20GiB disk, NAT networking, claude-code harness). -
No cloud-init. StereOS images are pre-built with stereosd and agentd. Runtime provisioning (secrets, mounts, agent config) happens over vsock, not via cloud-init ISOs.
Conventions
-
Error handling: Wrap errors with
fmt.Errorf("context: %w", err). Include actionable guidance in user-facing errors (e.g., "install QEMU: brew install qemu"). -
Output: Use
ui.Status(),ui.Success(),ui.Warn(),ui.Error(), andui.Info()for user-facing messages. Write to stderr so stdout stays clean. -
Cleanup on failure: If
Up()fails partway through creating VM resources, remove the VM directory (os.RemoveAll). Follow this pattern for any operation that creates resources. -
Process management: Use
syscall.Signal(0)to check if a PID is alive. Use SIGTERM before SIGKILL. Read PIDs from the QEMU pidfile. -
Testing: Config parsing is tested in
pkg/config/config_test.go. VM and SSH packages require QEMU/network and use manual smoke testing.
File layout on disk
~/.config/mb/
├── config.toml # Optional persistent config (read by Viper)
├── mb.sock # Daemon unix socket (runtime)
├── daemon.pid # Daemon PID file (runtime)
├── mixtapes/
│ └── <name>/
│ └── nixos.img # StereOS raw image (or nixos.qcow2)
└── vms/
└── <name>/
├── state.json # Metadata (name, ports, cpus, etc.)
├── jcard.toml # Copy of the sandbox configuration
├── disk.raw # VM disk (copied from mixtape)
├── disk.qcow2 # Or qcow2 overlay (if base is qcow2)
├── efi-vars.fd # Writable EFI variable store (64MB)
├── qmp.sock # QMP unix socket (exists while VM runs)
├── serial.log # Serial console output
├── qemu.pid # QEMU process ID
├── vmhost.sock # Vmhost control socket (runtime)
├── vmhost.pid # Vmhost process ID (runtime)
└── vmhost.log # Vmhost process log output
Common tasks
Adding a new command
- Create
cmd/<name>/<name>.gowith package<name>cmder. - Implement
New<Name>Cmd(configDirFn func() string) *cobra.Command. - Register it in
main.gowithcmd.AddCommand(...), passingmbconfig.ConfigDir. - Use
client.New(configDirFn())to talk to the daemon. - For commands that need the daemon running, use the
ensureDaemon()pattern fromcmd/up/up.go.
Adding a new config field
- Add the field to the appropriate struct in
pkg/config/config.go. - Add a default in
applyDefaults()if needed. - Add validation in
validate()if needed. - If it's a path, expand it in
expandPaths(). - Add a test case in
pkg/config/config_test.go.
Adding a new daemon RPC method
- Add the method constant in
pkg/daemon/rpc.go. - Add request/response fields as needed.
- Implement the handler in
pkg/daemon/daemon.go. - Add a client method in
pkg/client/client.go.
Changing the QEMU command line
Edit buildArgs() in pkg/vm/qemu.go. The full QEMU invocation is assembled
there from the Instance and JcardConfig. Platform-specific settings
(accelerator, machine type, EFI paths, vsock device) come from
QEMUPlatformConfig -- edit the platform backend files
(backend_darwin_arm64.go, backend_linux.go) to change those.
Note: QEMU platform config for vmhost is also defined in
cmd/vmhost/platform_darwin_arm64.go and cmd/vmhost/platform_linux.go
via the getPlatformConfig() function.
Debugging boot issues
cat ~/.config/mb/vms/<name>/vmhost.log # Vmhost process log
cat ~/.config/mb/vms/<name>/serial.log # Serial console output
socat - UNIX-CONNECT:~/.config/mb/vms/<name>/qmp.sock
{"execute": "qmp_capabilities"}
{"execute": "query-status"}