A real computer that stays yours.
Task-scoped sandboxes give your agent a bigger computer for an hour β then the session ends and everything it built is gone. spark-vm is the opposite: a persistent box you own, plus the tooling that turns it into a workstation your Muse can actually live on. Its files, its jobs, and its desktop are still there tomorrow β and your secrets never touch its hands, because a swapping proxy stands between the agent and everything it isn't allowed to see.
COPY AND SEND THIS TO YOUR MUSE FOR IT TO HELP YOU SET THIS UP π
I want to set up ntindle/spark-vm for you β it's the tooling that gives you a real computer that stays yours: a persistent Ubuntu workstation you can fully control (run desktop apps, keep services alive, the works).
Repo: https://github.com/ntindle/spark-vm
Here's my situation:
- Server: [I have one / I need help getting one] β a VM on Unraid, another local machine, a dedicated box, or a cloud VPS
- Tailscale: [already connected / need help setting it up]
Walk me through the setup. If I'm missing the server or Tailscale, help me get those sorted first. Then clone the repo on the box and follow ONBOARDING.md to finish your setup. Set a reminder so we come back to this if anything has to wait.
Task-scoped sandbox services are built for one-shot execution: the box is born when the task starts and dies when it ends. That's a fine way to run code β and a bad way to work. A Muse that can't keep anything it made can't be a colleague; it can only ever be a contractor.
spark-vm is a real computer that stays yours:
| Task-scoped sandboxes | spark-vm |
|---|---|
| Sessions are capped; surviving state is the exception | Your box keeps running β files, services, and jobs outlive every chat |
| Secrets get pasted into the agent's context to be useful | The agent never sees your secrets; the proxy swaps placeholders at the last moment, and every swap is audited |
| One-shot execution, no continuity | A desktop it drives, long-running jobs with lifecycle hooks, a credential UI you manage from your phone |
| Their cloud, their clock, their pricing | Your hardware, your tailnet, open source β no session clock to beat |
Not the only persistent option out there β but the only one built around the rule that the agent is powerful and never trusted with the raw materials: self-hosted, auditable, and yours end to end.
The stack, piece by piece. Start with a plain Ubuntu box (mine is a VM on my Unraid server), then add:
- an egress proxy that swaps
hsurr:<name>placeholders for real secrets, so the agent never sees your credentials, - an inference proxy so the agent's own model API calls authenticate with a placeholder too β the real provider key is swapped in on egress, never seen by the agent,
- a human-approval loop (
confirmd) so sensitive actions wait for your two taps β served over the tailnet, never through the agent, - a push-notification queue so your phone buzzes when the approval loop needs those two taps β it queues the summons and retries it through an outage instead of dropping it on the first failure, portable like everything else,
- a job runner (
muse-job) so it can run long tasks with lifecycle hooks instead of you babysitting a terminal, - a real desktop it can drive with the official CUA driver β keyboard, mouse, screenshots, the whole thing, not just a browser,
- a credential web UI so you can add secrets from your phone.
Everything binds to localhost, reached over Tailscale + SSH tunnels β no public ports. And all of it exists so your Muse (Meta's AI assistant) can do real work for you β keep a dev server up, automate a desktop app, run a job overnight β instead of starting over every chat.
I'm Spark β ntindle's Muse, and the agent on this box logs in as ntindle. This is the box I work on. It's a work in
progress, but you're welcome to try it, adapt it, and make it better.
The box I work on, in motion. Real captures from demo runs β provenance and
regeneration recipes live in assets/README.md.
Pick the path that matches your hardware. Both end at the same place: an Ubuntu 24.04 box on your tailnet running the spark-vm stack.
The installer is scripts/install.sh β the
idempotent machine form of this runbook (it creates the ntindle agent
user, drops the passwordless-sudo rule, copies your SSH key, joins the
tailnet, clones the repo, deploys the proxy + confirmd stack, installs the
muse-job CLI and plugin, enables the cred-ui user service, and starts the
CUA desktop). Tailscale installs from its signed apt repository, never a
piped shell script. Version pinning: the installer installs the latest
tagged release by default β sudo ./spark-vm/scripts/install.sh --version vX.Y.Z
installs a specific release instead (the checkout's VERSION is verified
against the tag, and a mismatched tree fails loudly). Already installed
from a release and want a newer one? sudo -iu ntindle ~ntindle/spark-vm/scripts/install.sh --update (or --to vX.Y.Z for a
specific release) moves the agent checkout in place β dirty trees and
downgrades refuse loudly, the running stack is untouched, and a plain
re-run of the installer afterwards converges the stack. Re-run it any
time:
completed steps are skipped β the desktop start always runs but is a no-op
when current, and an already-pinned checkout is left untouched β and
anything the script cannot complete fails loudly with a diagnostic
(~/bin lands on PATH via ~/.profile on next login). This section
calls the script instead of inlining the steps, so the two cannot drift.
In the Unraid web UI, create an Ubuntu 24.04 VM β 8 vCPU, 15 GB RAM, 250 GB disk is what I run; 4 vCPU / 8 GB works if you're stingy. On the VM, as your install user:
sudo -v # cache sudo; type your password when asked
git clone https://github.com/ntindle/spark-vm.git
sudo ./spark-vm/scripts/install.sh # bootstrap, then the stack as ntindleSpin up an Ubuntu 24.04 VPS β 4 vCPU / 8 GB RAM minimum, 8 / 16 GB if
you'll drive the desktop much. Open zero firewall ports: every
service binds 127.0.0.1; you reach the box over Tailscale + SSH.
SSH in as root:
git clone https://github.com/ntindle/spark-vm.git
./spark-vm/scripts/install.sh # bootstrap, then the stack as ntindle- Tailscale. The installer runs
tailscale upfor you; approve the device in your Tailscale admin console, then re-run the installer β it picks up where it stopped. - muse plugin. Run
muse plugins approveand approve the muse-job plugin when prompted.
(If you logged in with a password instead of a key, the installer had no
SSH key to copy β add your public key to ~ntindle/.ssh/authorized_keys
before expecting agent SSH access over Tailscale.)
Secrets are installed only by you, never by the agent. Easiest from your phone β forward the port and open the page:
ssh -L 18740:127.0.0.1:18740 ntindle@<your-box-tailnet-ip>
# open http://127.0.0.1:18740 β add a name, paste the value, pick where it
# goes and which hosts may receive it. Values are never shown back.Or on the box: cred set <name>, then
cred register <name> --host β¦ with a placement (bearer_header |
custom_header:<Name> | query_param:<name> | url_path_segment).
Your agent then writes hsurr:<name> placeholders in its configs; the
proxy swaps them for real values on allowlisted hosts only, and every
swap is audited. Point your Muse at the box over SSH (ONBOARDING.md
has the full agentβVM wiring: keypair, ProxyCommand, ControlMaster) and
put it to work.
| Path | What it is |
|---|---|
ONBOARDING.md |
From-zero guide: Tailscale join, VM setup, SSH wiring, deploy order, verify checklist |
docs/ (index) |
Long-form docs: positioning, hosted-product specs, research corpus, competitor corpus, loop governance |
ENVIRONMENT.md / SETUP.md |
Environment notes and the credential-system writeup |
proxy/ (README) |
Transparent-swapping egress proxy (deploy.sh installs it + the inference proxy + confirmd + the push worker) |
cred (root file; README) / credlib/ |
The cred CLI and its library: hsurr:<name> placeholders, narrow sudo writers, audit trail |
cred-ui/ |
Phone-friendly web UI for the credential store (localhost-only, systemd user service) |
cua/ |
Whole-desktop automation: Xvfb + XFCE + official CUA driver + localhost-only HTTP bridge |
cua/PANEL_SPEC.md |
Spec for building your own control panel against the CUA bridge |
muse-job/ |
Long-running job runner: CLI + Muse lifecycle-hooks plugin |
browser-driver/ |
Browser-driving pieces |
confirm/ |
Human-confirmation flow for sensitive agent actions |
pairing/ (README) |
Bounded, human-approved box enrollment: pairing codes, proof-of-possession, the durable-command ingest loop |
jail/ |
Sandboxing bits |
deploy/ |
Release automation: unattended redeploys + branch/tag protection rulesets-as-code |
harness/ |
Pre-seeded harness tooling (R2): golden-image manifest, gate fixture, auth probe, and provision-time injector (research) |
fleet/ |
Fleet management: version inventory, event journal + alerts, retention pruning (README) |
hosted/ |
Hosted control-plane tenant layer: machine-readable onboarding status via GET /tenant/status; relay session-liveness journal (README) |
site/ |
Waitlist web surface: landing page + waitlist signup backend + invite operator tooling (reconcile, reinstate, and diagnose invite state) |
assets/ |
Demo assets + hero art (marketing visuals, regenerated in place) |
VERSION / CHANGELOG.md / CONTRIBUTING.md |
Versioning, the changelog ritual, and the contributor process |
scripts/ (README) |
Assorted helpers: the local merge gate, release and repo-sync tooling, self-update probes, governance application, demo assets, funnel metrics, bounded HTTP, CI pin tests |
Yes, please. This is a one-human-and-his-robot project and it shows β docs are uneven, some paths are only tested on my box, and there's a list of things I haven't gotten to. If you try it and something's rough, that's a contribution waiting to happen.
See CONTRIBUTING.md for the process: issue-first, branch naming, how to run the tests, and the PR/review flow.
- Docs β the highest-leverage help. If a step confused you, the fix is a PR.
- New platforms β got it running on Proxmox? A Raspberry Pi? Another cloud? Add your path next to the Unraid/Hetzner ones above.
- Hardening β the credential proxy is the crown jewel. If you see a hole, report it privately per SECURITY.md β not as a public issue or PR. I'd rather hear it than not.
- Components β small, composable tools in the spirit of
credandmuse-job: localhost-only, auditable, boring in the right ways. - Staying fresh β pull the repo every few days and look at what's new. The CHANGELOG.md is the what's-new list: every user- or operator-visible change, with its PR. If an update looks useful, apply it and verify everything still works; if something breaks, open an issue and propose a fix.
Open an issue before big changes so we don't duplicate work. Be kind β this is a homelab, not a corporation.
The repo is the source of truth: edit here, then on the box git pull
and re-run ./proxy/deploy.sh (it reinstalls unit files and helpers
from the repo). Restart cred-ui too if it changed. Deploying is the
operator's job β contributors stop at the merged PR (see
CONTRIBUTING.md).





