| Part | Choice |
|---|---|
| App | Rust 2024, GPUI 0.2.2 (Zed's GPU UI framework) |
| Isolation | Firecracker microVMs on KVM |
| Guest | Debian bookworm (from node:22-bookworm), built with Docker into an ext4 image |
| TLS to Anthropic | rustls with the ring provider, webpki-roots |
| Fonts | IBM Plex Sans and Plex Mono, embedded |
| Icons | Lucide (ISC), embedded |
The app has four other crates as dependencies: gpui, include_dir, rustls and webpki-roots.
src/
main.rs UI: title bar, setup, corn view, dialogs, window frame
sandbox.rs Corns: create, start, stop, delete, terminal attach, file copy, export
gate.rs Network gate, Claude proxy, safe tar reader, activity log
share.rs Shared folder: git bundles in and out, review, apply approved files
skills.rs Skills: copy picked skills from ~/.claude/skills into a read-only image for every corn
theme.rs Light and dark palettes, appearance setting
icons.rs Embedded assets
guest/
Dockerfile The workbench image
init PID 1, stage one: overlay the corn disk on the read-only image
stage2 PID 1, stage two: account, vsock bridges, channels
channels.sh Inbox, outbox and shared-folder channels
profile.sh Proxy settings and the placeholder Claude credential
scripts/
fetch-firecracker.sh Firecracker, jailer and guest kernel
build-image.sh Build golden.ext4 from guest/
assets/ Fonts, icons, desktop entry
vendor/xattr Patched xattr 0.2 (libc dropped ENOATTR)
sudo apt install -y build-essential clang pkg-config cmake \
libwayland-dev libxkbcommon-dev libxkbcommon-x11-dev libvulkan-dev \
libfontconfig1-dev libfreetype-dev libx11-dev libxcb1-dev libssl-dev libzstd-dev \
socat e2fsprogs bubblewrap git docker.ioInstall Rust with rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y- KVM. Your user must read and write
/dev/kvm. Check withls -l /dev/kvm. - Docker. Popcorn needs Docker only to build the image.
- A terminal app. Popcorn tries Ptyxis, GNOME Terminal, Console, Konsole, Alacritty, kitty,
x-terminal-emulator, then xterm.
Popcorn runs on Linux only. The UI also builds on macOS and Windows, but corns need KVM.
./scripts/fetch-firecracker.sh # one time
./scripts/build-image.sh # one time, and after each change in guest/
cargo build --release
./target/release/popcorn
./install.sh # optional: install to ~/.local/bin with a desktop entryRebuild the image after you change a file in guest/. Running corns keep the old image until
you stop and start them.
A release has four files: an AppImage, a .deb, the compressed workbench image and SHA256SUMS.
| Part | Where it comes from |
|---|---|
| App version | version in Cargo.toml |
| Workbench image | golden-<version>.ext4.zst, built by scripts/release-image.sh |
| Image hash | Compiled into the app from POPCORN_IMAGE_SHA256 |
| Firecracker, jailer, kernel | Pinned with their SHA-256 in scripts/fetch-firecracker.sh, inside both packages, in /usr/lib/popcorn |
To make a release, change the version in Cargo.toml, commit, then push a tag:
git tag v0.1.0 && git push origin v0.1.0The release workflow builds the image, tests, packages and publishes. To build the same files
locally:
./scripts/fetch-firecracker.sh
./scripts/release-image.sh # dist/golden-<version>.ext4.zst and its .sha256
./scripts/package.sh all # dist/Popcorn-<version>-x86_64.AppImage, dist/popcorn_<version>_amd64.debHow images work in a release:
- On first run, the app downloads the image for its own version from the GitHub release.
- The app checks the SHA-256 of the download against the hash compiled into it. It unpacks nothing before the check passes.
- Each version has its own file,
images/golden-<version>.ext4. A corn records its image. An update does not change the image under an existing corn. - A development build (no
POPCORN_IMAGE_SHA256) builds the image locally with Docker. POPCORN_IMAGE_URLreplaces the download address, for example for a mirror.
The download needs a public release. For a private repository, set POPCORN_IMAGE_URL to an
address that does not need a login.
| Path | Contents |
|---|---|
~/.local/share/popcorn/bin/ |
firecracker, jailer |
~/.local/share/popcorn/vmlinux |
Guest kernel |
~/.local/share/popcorn/images/golden-<version>.ext4 |
Read-only workbench image of a release |
~/.local/share/popcorn/golden.ext4 |
Read-only workbench image of a development build |
~/.local/share/popcorn/allow |
Hosts you always allow, one for each line. All corns use this list. |
~/.local/share/popcorn/corns/<corn>/ |
disk.ext4, image, vm.json, console.log, activity.log, mode, title, inbox.sent, share, terminal.lock, sockets. Older versions used cases/; Popcorn moves it. |
~/.local/share/popcorn/skills/, skills.ext4 |
Skills from the last sync, and the read-only image that every corn gets as a third drive |
~/.config/popcorn/ |
user, claude-key (mode 0600), appearance, claude-skipped, skills (the picked skills) |
The corn has no network card. All traffic uses vsock. The host CID is 2.
| Port | Direction | Use |
|---|---|---|
| 1080 | corn to host | HTTP CONNECT proxy through the network gate. Public addresses only; 64 connections at a time. |
| 1081 | corn to host | Claude API. The host adds the token and opens TLS. |
| 1024 | host to corn | Login shell for the terminal |
| 1025 | host to corn | Files in, as a tar stream, to ~/inbox |
| 1026 | host to corn | Outbox export, as a tar stream |
| 1029 | host to corn | Shared folder in, as a git bundle, to ~/work/<name> |
| 1030 | host to corn | Shared folder back, as a git bundle of new objects, for review |
| Variable | Effect |
|---|---|
POPCORN_HOME |
Use another data folder. Keep the path short: Unix socket paths have a limit of 108 bytes. |
POPCORN_CLAUDE_KEY |
Use this token instead of the saved one. An empty value means no token. |
POPCORN_USER |
Use this account name in corns instead of the saved one. |
cargo test --release # unit tests, fastThe VM tests boot real corns. They need KVM, the image and network access. Run them one at a time, in a short temporary home:
H=$XDG_RUNTIME_DIR/pcdemo; mkdir -p $H
for f in golden.ext4 vmlinux bin; do ln -sfn ~/.local/share/popcorn/$f $H/$f; done
POPCORN_HOME=$H cargo test --release -- --include-ignored --test-threads=1
rm -rf $H| Test | Checks |
|---|---|
case_boots_isolated_and_gated |
No network card. Denied host refused. Internal addresses refused, literal or resolved. Allowed host works. Claude proxy swaps the token, works while sealed, and returns 401 with no token. Token not in the guest. Outbox export drops links. Shared folder: secrets stay home, changes detected, only approved files written. Runs Firecracker inside bubblewrap. A synced skill is in the corn. |
attach_is_responsive |
Terminal echo under 150 ms, TERM set, prompt shows the user name. |
internal_addresses_are_not_public |
Loopback, private, link-local, metadata, multicast and mapped IPv6 addresses are refused; public ones pass. |
import_keeps_only_plain_files_inside_quarantine |
Tar reader drops links, devices and unsafe paths. |
shares_through_git_and_trusts_nothing_back |
Ignored files stay home. Other version control folders stay home, and a folder with .hgignore rules is refused until they are in .gitignore. The corn's links are never offered. Your own edits are flagged. New files are never executable. Applied changes are not offered again. A forged .git path and garbage bundles are refused. |
syncs_only_plain_files_of_chosen_skills |
Only folders with a SKILL.md sync. A skill that is a link is followed; links inside a skill are not. No setuid bits. A skill removed from your computer is dropped. An empty sync still makes an image, so corns remove old skills. |
writes_stay_inside_the_folder |
Approved writes never leave the folder or go through a link. |
refuses_home_and_secret_folders |
Home, /, folders above home and folders with keys are refused. |
terminal_drops_clipboard_escapes |
Clipboard escapes from the corn never reach your terminal, in any form or split; other output passes. |
landlock_allows_only_what_is_named (in tests/) |
popcorn landlock allows only the named files and programs. Your home folder, writes, TCP and signals to other programs are refused. |
names_become_safe_account_names |
Typed names become safe Linux account names. |
- A corn does not boot. Read
corns/<corn>/console.log. It has the guest kernel and init output. - A connection fails. Read
corns/<corn>/activity.log. Each line has a time, a verdict and a target. - The terminal does not connect. Make sure the corn runs, then run
popcorn attach <corn folder>in a terminal. - Screenshots on GNOME Wayland. GNOME blocks screen capture. Run Popcorn under XWayland with
env -u WAYLAND_DISPLAY, then capture the window withffmpeg -f x11grab -window_id <id>.
- Text in the app. Use simple English close to ASD-STE100: short sentences, active voice, one instruction for each sentence. Use hints, not explanations.
- Shortcuts in code. Mark a known limit with a
ponytail:comment. Say the limit and the fix. - Security code. Most guarantees in the README have a test. Add a test when you add or change a guarantee.