Skip to content
ntindlePublic

About

Setup + tooling for the spark-vm dev box

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

spark-vm logo

spark-vm

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 prompt

Spark, hard at work next to the 12U homelab rack

Let your Muse do it

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.

Why a computer, not a sandbox

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.

What's in the box

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.

See it in action

The box I work on, in motion. Real captures from demo runs β€” provenance and regeneration recipes live in assets/README.md.

Animated GIF β€” terminal transcript: the agent's config holds only hsurr: placeholders, the proxy's audit journal names the placeholder, and a grep for secret-length tokens finds nothing.
Egress proxy β€” secrets the agent never sees. The config holds only placeholders; the audit journal names the placeholder, never the value.
Animated GIF β€” terminal transcript: one command spawns a job, a later status shows the same session still alive.
Job runner β€” long jobs stay alive. One command spawns a job; the session stays up β€” status, 20+ minutes later, still healthy.
Animated GIF β€” phone-width frames: a pending approval list, its detail page, the two-tap arm step, then the cleared list.
Human-approval loop β€” pending β†’ detail β†’ two taps β†’ cleared.
Animated GIF β€” phone-width frames: the credential add/update form at 390px, then the stored-credentials list.
Credential web UI β€” the add/update form at phone width; the list shows names and state only. Values are never shown back.
Animated GIF β€” terminal transcript: enqueue the summons, the worker's first delivery attempt hits a dead endpoint (500) and reschedules, the journal keeps the pending entry, and the retry delivers after the endpoint recovers (201).
Push queue β€” the summons survives an outage. The worker's first delivery attempt hits a dead endpoint (500); the durable journal keeps the approval and the retry delivers (201).
Animated GIF β€” two terminal frames: Xvfb's birth record showing 4 days elapsed, then the same pid, days old, re-verified alive with its job sessions intact.
Real desktop β€” one session, four days old: Xvfb's birth record, then the same pid re-verified alive with its job sessions intact.

Try it

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.

Option A: you have Unraid

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 ntindle

Option B: Hetzner (or any cloud VPS)

Spin 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

Then: the two human gates, in order

  1. Tailscale. The installer runs tailscale up for you; approve the device in your Tailscale admin console, then re-run the installer β€” it picks up where it stopped.
  2. muse plugin. Run muse plugins approve and 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.)

Then: give it secrets (the human part)

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.

Repo layout

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

Contributing

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 cred and muse-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.

Pushing changes (operators)

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).

About

Setup + tooling for the spark-vm dev box

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages