isopod

Contributing to isopod

Thanks for your interest in isopod. This guide covers building the project from source, running the tests, the crate layout, coding conventions, and how to propose changes.

isopod is pre-1.0; main is the active development line. Please open an issue to discuss a substantial change before investing a lot of work in it — the design in PLAN.md is the reference for what is in scope for v1 versus the v2+ backlog.

Security bugs do not go through the public issue tracker. See SECURITY.md for private reporting.


Prerequisites

isopod targets Linux with KVM. You need:

bash git submodule update --init --recursive

On a Debian/Ubuntu host these come from nftables iproute2 e2fsprogs squashfs-tools build-essential (package names vary by distro).


Build

# Build the whole workspace (CLI, MCP server, core, guest agent) in release mode.
cargo build --release

This produces three binaries under target/release/:

(The guest agent, isopod-guest-agent, is also built as part of the workspace but is baked into the guest images by the image pipeline rather than run on the host.) The front ends are separate binaries built from a shared isopod-core library, so after changing crates/core you must rebuild whichever front end you are exercising (cargo build --release builds all of them).

To bring up a working environment (Firecracker binary, guest kernel, guest images), then run once:

./target/release/isopod dev build-fc        # build vendored Firecracker into ~/.isopod/bin
./target/release/isopod image fetch-kernel  # pinned, digest-verified guest kernel
./target/release/isopod image build-all     # every guest image incl. the squashfs bases (unprivileged)
sudo ./target/release/isopod setup          # one-time host networking (the only root step)

All runtime state lives under ~/.isopod (file-locked). Nothing is written outside your home directory except what sudo isopod setup provisions on the host network stack, which sudo isopod setup --remove cleans up.


Test

cargo test                # run the full workspace test suite
cargo clippy --all-targets --all-features   # lint; keep it clean
cargo fmt --all           # format (rustfmt)

A few notes on the test suite:

Dogfooding is a first-class gap-finding mechanism here: running real work through isopod itself surfaces issues that unit tests miss. Findings are logged in docs/dogfood-findings.md; if you discover a gap by using the tool, add an entry with a severity and a fix-or-file decision.


What CI enforces

flowchart LR
    PR["pull request"] --> VG["Version guard"]
    PR --> BT["Build and test"]
    PR --> DP["Dependency policy<br/>cargo-deny licenses, bans, sources"]
    PR --> WL["Workflow lint<br/>zizmor and actionlint"]
    CRON["daily cron"] --> ADV["RustSec advisories<br/>cargo-deny"]
    BOT["weekly Dependabot"] --> PIN["grouped PR bumping<br/>action hash pins"]
    MAIN["push to main"] --> COV["Coverage report<br/>advisory, never a gate"]
    TAG["tag push"] --> REL["Release packages"]
    REL --> SUM["SHA256SUMS"]
    REL --> ATT["provenance attestation"]

Every PR runs four independent gates (ci.yml); all four are deterministic — the same tree gives the same answer forever:

Off the PR path:

None of this runs the live suite. GitHub runners have no /dev/kvm, so the real-boot and egress-ledger tests still run only by hand — green CI proves the build, the dependencies, and the workflows, not that the sandbox holds.


Crate map

isopod is a Cargo workspace of eight crates. Solid arrows are Cargo dependencies; dashed arrows are runtime relationships that no Cargo.toml records:

flowchart TB
    CLI["crates/cli<br/>isopod"] --> CORE
    MCP["crates/mcp<br/>isopod-mcp"] --> CORE["crates/core<br/>isopod-core"]
    CORE --> FCC["crates/fc-client<br/>isopod-fc"]
    CORE --> PROTO["crates/proto<br/>isopod-proto"]
    GA["crates/guest-agent<br/>isopod-guest-agent"] --> PROTO
    CORE -.->|"execs it when ISOPOD_JAIL=1"| JAIL["crates/jail<br/>isopod-jail"]
    OCI["crates/oci-unpack<br/>isopod-oci-unpack"] -.->|"standalone: nothing depends on it yet"| NONE["image import<br/>not wired up — wave 1 is the extractor alone"]
    FCC -.->|"HTTP/JSON over a per-VM unix socket"| FCB["firecracker<br/>not a crate — the vendored VMM"]
    GA -.->|"baked into the image, runs as PID 1"| VM["guest microVM<br/>not a crate — the sandbox itself"]

isopod-proto is the only crate compiled into both sides of the boundary — the host links it through isopod-core, the guest through isopod-guest-agent — which is why its PROTO_VERSION gates host/guest compatibility.

Path Package Responsibility
crates/fc-client isopod-fc Typed client for the Firecracker management API, pinned to the v1.16.1 Swagger. One HTTP client per VM over its API unix socket, with a runtime pre-boot/post-boot phase guard, process supervision (kill_on_drop, spawn-in-slot), and hybrid-vsock helpers. Deliberately dependency-light so it can be extracted as a standalone SDK.
crates/core isopod-core All orchestration logic. Modules: vm (lifecycle: spawn Firecracker, configure via the API, run, reap), stage (commit/fork/stack/gc of the content-addressed store), snapshot (warm-pool save/restore + cache invalidation), net (tap slot claim/release), agent (guest-agent vsock RPC client), store/paths (~/.isopod on-disk state), image (kernel + rootfs pipeline), names (vanity/stage naming).
crates/proto isopod-proto The host↔guest RPC contract: length-prefixed serde-JSON frames over vsock, one connection per operation. PROTO_VERSION is exchanged in the ping handshake so mismatched host/guest pairs fail fast.
crates/guest-agent isopod-guest-agent Static musl binary that runs as PID 1 in the guest: mounts pseudo-filesystems and the overlay, pivot_roots, resyncs the clock, reaps zombies, and serves the exec/file/configure RPC on vsock port 52.
crates/cli isopod The isopod binary — run, stage, vm, warmpool, setup, image, dev. One-shot argv + JSON.
crates/mcp isopod-mcp The rmcp 2.2 stdio MCP server exposing sandbox_run, stage_list/stage_info/stage_rm, vm_list/vm_gc. A thin async shim over isopod-core.
crates/jail isopod-jail The rootless microjail helper that wraps each Firecracker process when ISOPOD_JAIL=1: user/pid namespaces, minimal bind-mount chroot, per-VM cgroup v2 caps.
crates/oci-unpack isopod-oci-unpack Confined extractor for OCI image layer tars: a directory-fd walk with O_NOFOLLOW that applies untrusted layers, whiteouts and all, without following a symbolic link a layer planted. Depends on nothing in the workspace and nothing depends on it yet — it is deliberately reviewable on its own, because it is the one component that writes attacker-authored bytes onto the host before any VM exists.

Supporting directories: images/ (checked-in kernel config, image build inputs), skill/ (the Claude Code workflow skill), docs/ (getting started, MCP usage, engineering logs), vendor/firecracker/ (the pinned submodule).


Coding conventions


Versioning

The project follows Semantic Versioning with the usual pre-1.0 reading: while the leading version is 0, a minor bump signals a feature wave or any breaking change (new subcommands, guest-image format changes, host↔guest protocol cutovers), and a patch bump signals backward-compatible fixes only.

Historical note: versions v0.2.0v0.6.0 were tagged retroactively at each milestone-close commit (M2–M6) when the versioning discipline was adopted at v0.7.0; the manifests at those commits still read 0.1.0.


Proposing changes

  1. Branch off main. Do not commit directly to main.
  2. Keep changes focused. Solve the stated problem; avoid unrelated refactors in the same change. Update tests and any affected docs (README.md, docs/, PLAN.md milestone notes) in the same change.
  3. Run the checks locally before opening a PR: cargo fmt --all, cargo clippy --all-targets --all-features, cargo test. If the change adds or updates a dependency, also run cargo deny check licenses bans sources (cargo install cargo-deny).
  4. Write a clear PR description explaining what changed and why, how you verified it (including any live VM/dogfood testing), and any follow-ups you are deliberately leaving out of scope.
  5. Reference the relevant PLAN.md milestone or backlog item where applicable, so reviewers can place the change in the roadmap.

Commit messages should be descriptive and explain the why, not just the what. There is no mandated trailer format.

By contributing, you agree that your contributions are licensed under the project's Apache License 2.0.

Rendered from CONTRIBUTING.md on the main branch.