Orbit — technical overview
Autonomous agent infrastructure
on OrbStack
Built by Rexford Machu. A local MCP server so an agent can own its SSH keys, spin up isolated Linux VMs and nested microVMs, run containers, and tear them down — secrets stay in an encrypted vault on the Mac.
I built a local agent runtime that keeps credentials under user control, discovered OrbStack’s SSH multiplexer broke per-VM identity isolation, replaced it with direct SSH, then added a separate-kernel QEMU path for eBPF and a declarative state reconciler.
macOS + OrbStack only. Single cgo-free Go binary. MIT licensed. Not affiliated with other projects named Orbit.
Architecture
Four execution modes, shared plumbing
Work runs as a VM, a container, a nested microVM, or a declared state document. Exec, vault, and audit are shared — not extra modes.
MCP client (e.g. Claude Code)
│ stdio JSON-RPC
▼
orbit binary
VM · container · microVM · state
exec.Resolver · vault · audit
│
orbctl / docker / QEMU TCG
SSH to <vm>.orb.local:22
or <carrier>.orb.local:<port>
vm
orbctl wrapper + multi-distro provisioner. Isolated machines, no macOS file sharing.
container
docker + compose. Inline YAML on stdin. Vault secret_env. Starts in 1–3s vs 30–60s for a VM.
microvm
Nested QEMU/TCG guest with its own kernel, for eBPF. Same exec / file_* tools by name.
state
JSON desired state. state_plan diffs; state_apply converges; state_destroy tears down only what is named.
exec (plumbing)
A Resolver maps an id to a VM or nested guest. Direct SSH, in-memory Ed25519 signer.
vault + audit (plumbing)
AES-256-GCM + Argon2id SQLite. JSONL audit log records names, never secret values.
Security model
Security invariants — and two deliberate bounds
KEY
Private keys never touch disk
Ed25519 keys decrypt into process memory for one SSH connection, then GC. The Go client takes an in-memory ssh.Signer — no temp files.
ARGV
Secret values never appear in argv
secret_env maps env-var name → vault key. Docker gets a bare -e NAME flag; the value lives only in the subprocess environment. Invisible to ps.
AES
Vault is encrypted at rest
Argon2id (salt=32B, m=64MiB, t=3, p=4). Fresh 12-byte IV per write. Auth tag detects tampering. Derived key never persisted. Passphrase from Keychain or ORBIT_PASSPHRASE.
GET
cred_get returns values to the caller
Including the model. That is intentional. cred_list / key_list return names only. Prefer secret_env when a secret must stay out of the transcript.
SSH
SSH host keys are trust-on-connect
InsecureIgnoreHostKey because traffic stays on loopback (.orb.local or a carrier-local port). TOFU pinning is planned and required before anything is reachable off-loopback (ADR-011).
VM lifecycle
VM lifecycle: vm_create
generate Ed25519 key
→
orbctl create --isolated
→
provision via orb root
→
SSH ready
Multi-distro provisioner
A single POSIX sh script auto-detects the package manager and init system:
aptapkdnfyumzypperpacmanxbps
systemdOpenRCrunit
No cloud-init — OrbStack only ships it for Debian/Ubuntu. Alpine, Fedora, and Arch all work (ADR-003). NixOS is the known exception.
Why direct SSH, not the multiplexer
OrbStack's 127.0.0.1:32222 multiplexer uses a single host key and ignores per-VM authorized_keys. Using it collapses every VM onto one identity. Orbit dials <vm>.orb.local:22 so each machine keeps its own keypair (ADR-002).
Containers
Container tools + secret injection
agent calls container_run(image="postgres:16", secret_env={"POSTGRES_PASSWORD": "prod-db-pw"})
orbit reads vault.GetCredential("default", "prod-db-pw") → value (in memory)
orbit builds argv: docker run -d -e POSTGRES_PASSWORD postgres:16 ← no value in argv
orbit sets subprocess env: POSTGRES_PASSWORD=<value> ← value here only
audit logs {"secret_env": ["POSTGRES_PASSWORD"]} ← name only
Lifecycle
container_run container_exec container_logs container_list container_stop container_rm
Compose stack
compose_up compose_down compose_logs compose_ps compose_exec compose_build — file path or inline YAML via -f -
Images
image_build — host context or inline Dockerfile on stdin. No temp files.
Nested microVMs
Own kernel, for eBPF — nested QEMU/TCG
On Apple Silicon every OrbStack machine shares one kernel. An eBPF program loaded “inside a VM” attaches to that shared kernel. Isolation requires a nested guest.
macOS
→
OrbStack carrier --isolated
→
QEMU TCG → guest kernel
Why TCG, not KVM
There is no /dev/kvm inside an OrbStack machine. TCG is slow and correct. Default alpine boots direct-kernel in tens of seconds; ubuntu is a full cloud image with a BTF kernel for CO-RE eBPF (minutes). Carrier must be Debian/Ubuntu (ADR-012).
Same exec path
QEMU forwards <carrier>.orb.local:<port> into guest:22. A microvm.Resolver chains onto the vault resolver, so exec_script vm_id=bpf-lab just works. Tools: microvm_create list start stop destroy.
Desired state
Declare what should exist
Instead of a script of create/run/destroy calls, the agent submits a JSON document. Orbit diffs it against live resources and converges.
{
"vms": [{ "name": "worker", "distro": "alpine" }],
"containers": [{ "image": "postgres:16", "name": "db", "ports": ["5432:5432"] }],
"composes": [{ "project": "api", "yaml": "services:\n web:\n image: nginx" }]
}
state_plan
Dry-run. Returns the action list (create / run / up / noop) without touching anything.
state_apply
Creates missing VMs, runs missing containers, brings up compose stacks. Already-running resources are left alone.
state_destroy
Tears down only what the document names. VMs purge their keys. Nested microVMs stay on the microvm_* tools.
Tools surface
40 MCP tools across 7 groups
vm_*vm_createvm_listvm_startvm_stopvm_destroy
key_*key_generatekey_listkey_rotatekey_revokekey_export_public
exec / fileexecexec_scriptexec_parallelfile_pushfile_pull
cred_*cred_setcred_getcred_listcred_delete
containercontainer_*compose_*image_build
microvm_*microvm_createmicrovm_listmicrovm_startmicrovm_stopmicrovm_destroy
state_*state_planstate_applystate_destroy
Key decisions
Architecture decision records
ADR-001Go over TypeScript — in-memory SSH signers, stdlib crypto, single cgo-free binarycore
ADR-002Direct SSH to <vm>.orb.local:22 — multiplexer collapses credentialssecurity
ADR-003Root provisioning script — cloud-init absent on Alpine / Fedora / Archportability
ADR-004AES-256-GCM + Argon2id vault in SQLite — local, encrypted, single filesecurity
ADR-005Container tools with vault secret injection via bare -e NAMEsecurity
ADR-006compose_exec via container resolution — avoids stdin conflict with inline YAMLdesign
ADR-007No direct uuid dep — crypto/rand hex; uuid remains an MCP-SDK transitivedeps
ADR-008Inline compose YAML via stdin (-f -) — no temp filesdesign
ADR-009JSONL audit log now — slog / metrics / OTel deferred to ROADMAP §4scope
ADR-010Vault passphrase via macOS Keychain with env override and auto-generationux
ADR-011SSH host-key trust-on-connect — bounded by loopback; TOFU pinning latersecurity
ADR-012Nested QEMU/TCG guests with their own kernel — eBPF cannot share OrbStack'ssecurity
Outcomes and evidence
What shipped, and how you can check
54–80%
core package coverage
Verify locally
go test ./... and go vet ./... pass without OrbStack (fakes for orbctl, docker, SSH). Opt-in: go run ./cmd/smoke against a real VM; ORBIT_DOCKER_E2E=1 go test ./internal/container/ for vault injection.
What that coverage means
Vault, exec, VM, container, microvm, state, keys, and audit are unit-tested. TestAllSpecToolsRegistered asserts all 40 tools, including the five microvm_* tools. Smoke and Docker E2E are opt-in, not in CI. No public GitHub Release yet — build from source.
Limitations
What this is not
- macOS + OrbStack only. Keychain bootstrap and
orbctl are host-specific. Other platforms need ORBIT_PASSPHRASE and still cannot drive VMs without OrbStack.
- Local loopback trust. SSH host keys are trust-on-connect. Fine on
.orb.local; not for off-machine exposure (ADR-011).
cred_get is a transcript leak by design. Use secret_env when the model must not see the value.
- Nested guests are slow. No
/dev/kvm → QEMU TCG. Alpine boots in tens of seconds; Ubuntu with BTF takes minutes. Correct isolation, not throughput.
- NixOS is unsupported by the auto-detect provisioner. Declarative
configuration.nix is ROADMAP §2.4.
- No GitHub Release yet. Homebrew formula is a SHA256 template.
go install …@latest works only after the module is published.
Distribution
How to build and ship it
From source (what to use today)
Go 1.25+. Unit tests use fakes — no OrbStack required. cmd/smoke is the opt-in real-VM path.
git clone https://github.com/machugram/orbit.git
go test ./...
go build -o bin/orbit ./cmd/orbit
claude mcp add orbit -- $PWD/bin/orbit
Releases and Homebrew
A v* tag is intended to build arm64 + amd64. No release exists yet. The in-tree Homebrew formula is a template — placeholder SHA256s. Do not brew install it yet. go install …@latest after the module is on the proxy.
git tag v0.1.0
git push origin v0.1.0
# then fill Formula/orbit.rb checksums
cgo-free
needs OrbStack on PATH