hermetic

hermetic — the missing Ruby sandbox. Pick your isolation strength (gVisor, a Firecracker microVM, a hosted sandbox, or hardened Docker) behind one run call, and run it off your app host so an escape can't reach your database or secrets.

Ruby today has exactly one honest option for running untrusted or model-generated code: a locked-down docker run. $SAFE was removed in Ruby 3.0; there is no Ruby client for E2B/Daytona/Modal and no Ruby wrapper for Firecracker or gVisor. hermetic fills that whitespace as an ecosystem building block: one interface, swappable isolation backends, zero runtime dependencies (stdlib only).

Install

gem "hermetic"

Usage

box = Hermetic.hosted(:e2b, api_key: ENV["E2B_API_KEY"])

result = box.run("python -c 'print(2+2)'",
                 files:   { "data.txt" => "..." },   # materialized in the workdir
                 stdin:   nil,
                 env:     { "MODE" => "batch" },      # guest env is default-deny otherwise
                 network: :none,                      # default deny; opt-in egress allowlist
                 timeout: 10,                         # host-enforced wall clock
                 limits:  { memory: "512m", cpus: "1", pids: 256 })

result.stdout       # => "4\n"
result.success?     # => true
result.timed_out?   # => false (a timeout maps to exit_status 124)
result.trust        # => :vendor

A String command runs via /bin/sh -c; an Array argv execs directly — no shell, no injection surface.

Backend matrix

Backend Constructor Isolation Trust Off-host?
Hosted (E2B) Hermetic.hosted(:e2b, api_key:) vendor microVM, in the vendor's cloud :vendor yes, by construction
Remote Hermetic.wrap(executor:, token:) or any local constructor + executor: whatever the executor runs (gVisor/Firecracker/Docker) :remote yes — dedicated sandbox host
Firecracker Hermetic.firecracker(kernel:, rootfs:) own guest kernel via KVM :vm no (pair with executor:)
gVisor Hermetic.gvisor(image:) user-space kernel (runsc), real syscall boundary :host no (pair with executor:)
Docker (hardened) Hermetic.docker(image:) container namespaces + caps/seccomp :host no — dev default
Null Hermetic.null none — disabled, fails loud on run :none no

Isolation is not the same thing as a trust boundary

Two independent axes, never conflated:

  1. Isolation strength — namespaces < gVisor < microVM. (Which backend.)
  2. Trust domain — does the guest run next to your RAILS_MASTER_KEY, database, and job queue, or in a separate security context?

A container escape on your app host lands next to everything you care about, however strong the container was. So every box (and every Result) carries the posture:

box.trust      # => :vendor | :remote | :vm | :host | :none
box.off_host?  # => true only for :vendor and :remote

And the one-line guard that keeps it honest:

raise "untrusted code requires an off-host sandbox" unless box.off_host?

Getting off-host is either free (hosted vendors) or one keyword: any local constructor takes executor: + token: and becomes a client for a dedicated sandbox host that holds no app secrets (see EXECUTOR.md for the server contract):

box = Hermetic.gvisor(image: "ruby:3.3-slim",
                      executor: "https://sbx.internal:8443", token: ENV["SBX_TOKEN"])
box.off_host?  # => true — same isolation tech, different blast radius

Credentials: the model never sees raw tokens

box = Hermetic.hosted(:e2b, api_key: ENV["E2B_API_KEY"],
  credentials: { "GITHUB_TOKEN" => -> { CredentialVault.fetch(:github) } })

Credentials are callables resolved lazily per run and injected into the guest's environment at exec time — never printed into the model-visible command, never cached on the box, never part of the idempotency key.

Silas drop-in

Hermetic::Result is a superset of Silas::Sandbox::Result (stdout / stderr / exit_status / success?), and every box implements the run / enabled? duck type Silas's config.sandbox already accepts — zero Silas changes:

Silas.configure do |c|
  c.sandbox = Hermetic.gvisor(image: "ruby:3.3-slim",
                              executor: "https://sbx.internal:8443",
                              token: ENV["SBX_TOKEN"])
end

Running Silas? Also require "hermetic/silas" — an optional shim that refuses to execute a sandbox run inside a ledger transaction (a sandbox exec is a non-replayable external effect; run_code stays at_most_once!).

In-doubt runs (remote/hosted)

If the wire fails after a request may have executed guest-side, hermetic raises Hermetic::TransportError with in_doubt?: true rather than silently retrying. Every run carries a deterministic Idempotency-Key, so a deliberate retry of the same logical run can be deduplicated executor-side. v1 never retries automatically.

Honest v1 scope

Shipped: Null, hardened Docker, gVisor, Hosted::E2B, Firecracker (pure config/jailer builders + injectable single-shot cold boot), Remote executor client with EXECUTOR.md, full run contract (files:/stdin:/env:/network:/ timeout:/limits:), :env credentials, trust/off_host?, the Silas shim.

Deferred (v1.1+):

  • :proxy credential mode (guest gets no token at all; egress proxy attaches auth)
  • Firecracker orchestration: warm pools/snapshots, vsock I/O, tap+nftables egress, rootfs builder — v1's default Firecracker runner refuses to boot; inject your own
  • Hosted Daytona/Modal (constructors name them and fail with the roadmap)
  • The executor server daemon itself (the client + wire contract ship now)
  • Persistent sandboxes, streaming stdout, artifact read-back

Known caveat: Hosted::E2B's endpoint/body shapes are a best-effort design-time approximation, to be pinned against the live API via the :hosted smoke layer before first real use.

Testing

The default suite is hermetic (the name is a promise): pure argv/VM-config/HTTP-spec builders plus injected fake runners and transports — no Docker, no network, no KVM.

bundle exec rspec                              # unit layer only, runs anywhere
HERMETIC_SMOKE=docker  bundle exec rspec       # + real `docker run` round-trips
HERMETIC_SMOKE=gvisor  bundle exec rspec       # + proves runsc boots (needs runsc runtime)
HERMETIC_SMOKE=hosted  bundle exec rspec       # + E2B round-trip (needs E2B_API_KEY)

Smoke specs are tagged :smoke, excluded unless HERMETIC_SMOKE is set, and skip themselves when their infrastructure is absent.

License

MIT.