cd ~/sagarpandita.com

experimental / september 2026

DeepSeek Harness in Firecracker on DGX Spark

DeepSeek Harness is exactly the kind of software I want to experiment with and exactly the kind of software I do not want directly running around my homelab host. It can ask a model for actions, execute shell commands, touch files, and generally behave like an agent rather than a chat client.

So I put the whole thing inside an ARM64 Firecracker microVM on my NVIDIA DGX Spark. The guest had no default Internet route, dependencies were staged from the host, and the only external egress I eventually allowed was the DeepSeek API through a host-side proxy.

host
DGX Spark
architecture
aarch64
firecracker
1.17.0
guest
Ubuntu 24.04
guest disk
50 GB sparse
package payload
1317 offline

the punchline

Firecracker is a very natural outer boundary for an autonomous agent. I did not try to teach DeepSeek Harness how to create Firecracker guests. I ran the entire Harness inside one. The model-facing application, its shell tools, its package lifecycle scripts, and whatever target workspace I give it all live on the disposable side of the KVM boundary.

Mac mini
   │
   │  Tailscale / SSH
   ▼
DGX Spark
   │
   ├── Firecracker + KVM
   ├── tap0 · 192.0.2.1
   ├── Squid · DeepSeek API allowlist
   │
   └── Firecracker guest · 192.0.2.2
          ├── Ubuntu 24.04 ARM64
          ├── Node 24
          ├── DeepSeek Harness
          ├── target workspace
          └── no default Internet route

The built-in Harness sandbox is still useful, but I wanted a stronger failure boundary around the whole process. If an agent manages to surprise me, I would much rather the surprise end at a microVM disk image than at the Spark host.

does firecracker run on spark?

DGX Spark is ARM64. Firecracker supports aarch64, but the more important prerequisite is working KVM access:

$ uname -m
aarch64

$ ls -l /dev/kvm
crw-rw----+ 1 root kvm 10, 232 /dev/kvm

$ [ -r /dev/kvm ] && [ -w /dev/kvm ] && echo "KVM READY"
KVM READY

With that green light, the native ARM64 Firecracker binary worked immediately:

$ ./firecracker --version
Firecracker v1.17.0

No emulation layer, no nested VM trickery — just Firecracker on the Spark's own KVM.

building the guest

I used Firecracker's ARM64 CI artifacts: an Ubuntu 24.04 SquashFS and a vmlinux-6.18.48 kernel. The root filesystem was unpacked, an SSH key was injected, and the result was written into an ext4 image.

$ unsquashfs ubuntu-24.04.squashfs.upstream

$ truncate -s 8G ubuntu-24.04.ext4

$ sudo mkfs.ext4 \
    -d squashfs-root \
    -F \
    ubuntu-24.04.ext4

The first image was logically 8 GB but only consumed about 445 MB on disk because it was sparse. Harness's dependency graph made 8 GB feel unnecessarily tight, so I later grew it to 50 GB:

$ truncate -s 50G ubuntu-24.04.ext4
$ sudo e2fsck -f ubuntu-24.04.ext4
$ sudo resize2fs ubuntu-24.04.ext4

A 50 GB sparse file does not immediately consume 50 GB of physical storage. It gives the guest room to breathe without preallocating the whole thing on the host.

boot & network boundary

The VM got 4 vCPUs, 8 GB of RAM, one root disk, and one TAP interface. I used the same simple private point-to-point layout throughout the experiment. The addresses below are documentation-only examples rather than the addresses from my actual network:

componentaddresspurpose
DGX Spark / tap0192.0.2.1host services, SSH path, later the egress proxy
Firecracker guest192.0.2.2DeepSeek Harness and agent execution

The boot itself was satisfyingly boring:

Ubuntu 24.04.4 LTS ubuntu-fc-uvm ttyS0

Welcome to Ubuntu 24.04.4 LTS
(GNU/Linux 6.18.48+ aarch64)

root@ubuntu-fc-uvm:~#

Then I intentionally stopped before adding a default route. The guest could reach the Spark:

root@ubuntu-fc-uvm:~# ping -c 3 192.0.2.1
64 bytes from 192.0.2.1: icmp_seq=1 ttl=64 time=0.349 ms
64 bytes from 192.0.2.1: icmp_seq=2 ttl=64 time=0.527 ms
64 bytes from 192.0.2.1: icmp_seq=3 ttl=64 time=0.352 ms

But not the Internet:

root@ubuntu-fc-uvm:~# ping -c 3 1.1.1.1
ping: connect: Network is unreachable

root@ubuntu-fc-uvm:~# curl -I https://github.com
curl: (6) Could not resolve host: github.com

That was not a networking bug. That was the policy.

installing harness without internet

The interesting problem now became installation. DeepSeek Harness is a large pnpm workspace with native dependencies. Giving the guest temporary unrestricted Internet access would have been easy, but it also would have defeated the point of the setup.

I used the Spark as a staging machine instead:

Internet
   │
   ▼
DGX Spark host
   ├── clone Harness
   ├── download Node 24 ARM64
   ├── download pnpm 11.7.0
   └── hydrate pnpm store
             │
             │  SCP / SSH over tap0
             ▼
Firecracker guest
   └── install + build while offline

The host fetched the lockfile's package payload into a dedicated store:

$ PNPM_CONFIG_STORE_DIR="$HOME/firecracker/dsh/pnpm-store" \
    node pnpm.cjs fetch --frozen-lockfile

$ du -sh ~/firecracker/dsh/pnpm-store
1.8G    $HOME/firecracker/dsh/pnpm-store

I copied the source without its giant Git history, then streamed the pnpm store into the guest. Inside Firecracker, the actual install reused the staged packages:

Packages: +1317
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 1317, reused 1312, downloaded 0, added 1317, done

downloaded 0 was the important line. The guest was installing from the content-addressable store, not silently reaching out for package tarballs.

native builds belong inside the vm

The minimal Firecracker Ubuntu image included Python, but not a compiler toolchain:

root@ubuntu-fc-uvm:~# python3 --version
Python 3.12.3

root@ubuntu-fc-uvm:~# make --version
-bash: make: command not found

root@ubuntu-fc-uvm:~# g++ --version
-bash: g++: command not found

Instead of compiling native Node dependencies on the host and copying a finished node_modules tree across, I downloaded the required Ubuntu ARM64 .deb files on the Spark and installed them inside the guest.

GNU Make 4.3
gcc 13.3.0
g++ 13.3.0
Python 3.12.3

This is a small but important distinction: downloading an archive on the host is very different from allowing dependency lifecycle scripts and native build steps to execute on the host.

pnpm's definition of offline

This produced my favorite debugging detour. Even after all package payloads were local, pnpm started doing this:

? Verifying lockfile against supply-chain policies (1607 entries)...
[WARN] GET https://registry.npmjs.org/@anthropic-ai%2Fsdk error (EAI_AGAIN)
[WARN] GET https://registry.npmjs.org/@aws-sdk%2Fcore error (EAI_AGAIN)
[WARN] GET https://registry.npmjs.org/@babel%2Fcore error (EAI_AGAIN)

The install had already linked the packages. pnpm was performing registry-backed lockfile / supply-chain verification afterward. Because the guest had no default route, the requests simply failed.

A second surprise: pnpm run build could trigger pnpm's dependency verification before actually running the build script. For this deliberately sealed environment I disabled that pre-run behavior:

pnpm_config_verify_deps_before_run=false \
pnpm run build

That finally got straight to:

$ tsx scripts/build.ts

the .git problem

I had intentionally excluded .git from the VM copy. Harness's build, however, wanted the current commit for client build metadata and tried to execute:

git rev-parse HEAD

With no Git binary and no repository metadata in the guest, the build failed with spawnSync git ENOENT. The clean fix was to obtain the exact SHA on the Spark and pass it into the guest:

$ git rev-parse HEAD
ddefc45fbc7f8e46dd73185e68295696d1297887

root@ubuntu-fc-uvm:~/deepseek-harness# \
export DSH_CLIENT_COMMIT_HASH="ddefc45fbc7f8e46dd73185e68295696d1297887"

root@ubuntu-fc-uvm:~/deepseek-harness# \
pnpm_config_verify_deps_before_run=false pnpm run build

After that, the ARM64 native module built and the TypeScript compilation proceeded normally.

two hops away from the ui

I was not even sitting at the Spark. I was SSH'd into it from a Mac mini over Tailscale, so reaching the Harness UI involved another layer:

Mac mini
Tailscale peer
    │
    │ Tailscale
    ▼
DGX Spark
Tailscale peer
    │
    │ private Firecracker link
    ▼
guest
192.0.2.2:3080

Keeping the UI bound to loopback and forwarding it over SSH meant I never had to expose Harness on the normal LAN interface.

controlled egress

A completely offline agent cannot call the DeepSeek cloud API, so the last piece was selective egress. I did not want to add a normal default route and hope the agent behaved. Instead, Squid ran on the Spark and listened only on the Firecracker-facing address:

http_port 192.0.2.1:3128

acl firecracker src 192.0.2.2/32
acl deepseek dstdomain api.deepseek.com
acl SSL_ports port 443
acl CONNECT method CONNECT

http_access allow firecracker CONNECT SSL_ports deepseek
http_access deny all

Harness was then launched with proxy variables pointed at the host:

HTTPS_PROXY=http://192.0.2.1:3128 \
HTTP_PROXY=http://192.0.2.1:3128 \
NO_PROXY=127.0.0.1,localhost,192.0.2.1,192.0.2.2 \
NODE_USE_ENV_PROXY=1 \
pnpm_config_verify_deps_before_run=false \
pnpm dsh web --no-open
destinationresult
192.0.2.1reachable host services / proxy
api.deepseek.com:443allowed through Squid
registry.npmjs.orgblocked
arbitrary Internetblocked

The best debugging tool at this point was not Harness itself. It was watching the boundary:

$ sudo tail -F /var/log/squid/access.log

$ sudo tcpdump -ni tap0 \
    'host 192.0.2.2 and (port 53 or port 80 or port 443 or port 3128)'

If a model call failed, I could immediately tell whether the VM tried the proxy, tried direct HTTPS, attempted DNS, or never emitted a packet at all.

what broke along the way

failurecausefix
xz: Cannot exec minimal guest did not include xz extract Node on the host and copy the directory / plain tar
npm: Cannot find module ../lib/cli.js npm launcher symlink was wrong after copying Node recreate npm / npx symlinks into lib/node_modules/npm
native addons would not build guest lacked make and g++ stage Ubuntu ARM64 .debs and install them inside the VM
pnpm hung on npm registry requests supply-chain verification still wanted registry metadata keep the guest offline and disable pre-run verification for the build
spawnSync git ENOENT source was copied without Git metadata set DSH_CLIENT_COMMIT_HASH explicitly
model transport failed guest had no path to the cloud API allowlist DeepSeek through a host-side proxy

findings

  • Firecracker runs cleanly on DGX Spark. ARM64 plus working KVM was enough; the native aarch64 Firecracker release booted Ubuntu without an emulation layer.
  • Run the whole agent harness inside the VM. This was simpler than trying to integrate Firecracker into Harness's own sandbox abstraction and gives a much clearer trust boundary.
  • No default route is surprisingly practical. The host can stage source, runtimes, compilers, and package stores into the guest over a private TAP link.
  • Native build steps should happen on the disposable side. Copy package payloads; do not move arbitrary dependency execution back onto the host just for convenience.
  • Package-manager "offline" modes are not always literally network-silent. pnpm still attempted registry-backed supply-chain checks after the package payloads were fully local.
  • Network observability is part of the containment story. Squid logs and tcpdump made it obvious what Harness was actually trying to reach.
  • The disposable image is the feature. Node, compilers, Harness, target workspaces, and anything the agent mutates can disappear with one ext4 file.

next experiment

The obvious next step is snapshot-driven agent testing. Build a clean guest once, snapshot it before handing control to the agent, run a task, collect the filesystem and network artifacts, then restore or destroy the VM.

clean microVM
     │
     ▼
pre-agent snapshot
     │
     ▼
copy target workspace
     │
     ▼
run DeepSeek Harness
     │
     ├── model API only
     ├── observe network
     └── observe filesystem
     │
     ▼
collect artifacts
     │
     ▼
destroy / restore

The expensive intelligence does not need to live inside the microVM. A local model can stay on the Spark host and expose only an API to the guest. The part that gets to execute arbitrary commands is the part I want to be disposable.

references