Local harness for running agent-driven tasks inside an LXD VM, without the worry of an agent damaging your host.
You describe a task, an LLM, and a VM in a single YAML file. mph (Multi-Purpose Harness) downloads the model, creates the VM, lets the LLM drive a tool-calling loop to complete the task, then hands the results back and tears the VM down.
- Config load: YAML is parsed, validated, and defaulted (
mph-vm,max_iterations: 10, timeouts, …). - Model init: llama.cpp runtimes + model (e.g.
unsloth/Qwen3-0.6B-Q8_0) are downloaded once and cached, then loaded locally. - VM lifecycle: the named LXD VM is launched (or reused with
-i), optionalcontent_diris copied to/home/ubuntu/content. - Agent loop: the LLM reasons, calls tools, reads results, and repeats until it answers with a final summary or hits
max_iterations. - Teardown: combined command output is printed (
----separated) and the VM is deleted unless-kwas passed.
Every command the agent tries to run is parsed as shell AST and checked against allowed_commands (fail-closed), so anything outside the allowlist never reaches the VM. See Restrict commands.
- LXD 5.x LTS or newer installed (deb or snap) with a running daemon
- VM images must include the LXD agent (official Ubuntu 20.04+ images do; 18.04 images require a manual agent install)
- Access to the LXD socket, typically by being in the
lxdgroup - A working default profile from
lxd init(a root disk on a storage pool, plus a network) - Go 1.27.2+ (only needed to build from source)
- A few GB free disk (VM image + model + llama.cpp runtimes)
- Hardware that can run a small LLM (CPU works for 0.6B to 1.7B models; a GPU helps for larger ones)
Releases are cut manually; each GitHub release attaches linux-amd64/linux-arm64
tarballs, and its tag makes the module installable at that version:
go install github.com/trollLemon/MPHarness/mph@latestCreate first-task.yaml:
vm:
name: mph-tutorial
disk: 10G
ram: 2G
cpu: 1
model: unsloth/Qwen3-0.6B-Q8_0
llm:
temperature: 0.2
top_p: 0.95
top_k: 40
agent:
max_iterations: 10
chat_timeout: 30m
total_timeout: 30m
prompt: |
Inside the VM, do the following:
1. Run ls /etc
2. Run uname -a
3. Summarise the results.Run it:
./bin/mph first-task.yaml
# or
./bin/mph -c first-task.yaml -v --prettyFirst run downloads the runtimes + model (cached afterwards). Follow the tutorial for a full walkthrough.
All run config lives in one YAML file. Only vm.*, model, and prompt are required; everything else has sane defaults.
vm:
name: mph-vm # optional, default mph-vm; DNS hostname rules, no underscores
disk: 20G
ram: 4G
cpu: 2
image: noble # optional Ubuntu image, default ubuntu:24.04
model: unsloth/Qwen3-0.6B-Q8_0
prompt: "test the build"
content_dir: ./my-build # optional: copied to /home/ubuntu/content in the VM
allowed_commands: # optional: omit = allow everything (shell binaries always blocked)
- coreUtils # meta-entry expanding to GNU coreutils
- apt
- git
llm: # optional sampling params
temperature: 0.2
top_p: 0.95
top_k: 40
tool_choice: auto # auto|required|none
context_window: 0 # 0 = auto-tune
agent: # optional loop bounds
max_iterations: 10
chat_timeout: 5m
total_timeout: 30m
secrets: # optional, specify which backend to use for resolving secrets
source: env # env|keyringFull reference: mph --help lists the config keys, but with illustrative values rather than defaults. See the guides for the real defaults. Guides:
- Configure the VM · Configure the LLM · Good models
- Restrict commands · Provide content
- Pass secrets · Manage VM lifecycle
mph [flags] <config.yaml>
-c, --config <file> path to YAML config (positional arg also works)
-i reuse an existing VM with the same name without prompting
-k keep the VM after the task instead of deleting it
-v, --verbose debug logging
--pretty human-readable console logs instead of JSON
--otel enable OpenTelemetry tracing (also MPH_OTEL=true)
--version print version and exit
-h, --help show helpNote: -i and -k have no long forms.
OTel is off by default. Enable with --otel / otel.enabled: true, pointing at an OTLP/gRPC collector (default localhost:4317):
make otel-up # Grafana+Loki+Tempo+Prometheus stack
./bin/mph --otel ./testing/test.yaml
# Grafana: http://localhost:3000 (admin/admin)See Enable OpenTelemetry.
go test ./... # or: make test
go vet ./... # or: make vet
golangci-lint run
make test-cover # coverage profile -> coverage.out- Tutorial: run your first task
- How-to guides: VM, LLM, commands, secrets, content, lifecycle, OTel
- Reference: architecture, models
- Explanation: tool calling, meta commands