Isolated execution sandboxes, as a library with no host-application concepts.
ex_sandbox is a composition and evidence layer over operating-system
facilities, not a new isolation mechanism. Nothing here invents containment:
the primitives are the OS's (cgroup v2, namespaces, bubblewrap,
sandbox-exec, Job Objects). What this library adds is a uniform behaviour
over them, a capability report that is honest about what the host cannot do,
and a conformance suite that establishes claims by observing breaches being
stopped rather than by confirming a limiter was invoked.
It depends on Elixir/OTP and nothing else — no Ash, no web framework, no host
application (FR-001).
Public interface
These modules are public. A breaking change to any of them is a major version
(FR-015):
ExSandbox— this module; the top-level APIExSandbox.Mechanism— the behaviour every isolation mechanism implementsExSandbox.Sandbox— the struct passed to every mechanism callbackExSandbox.Capability— the host capability report (FR-016)ExSandbox.Hardening— the OS-level enforcement seamExSandbox.Conformance— the conformance suite, included viauseExSandbox.Proxy— forwards a request to a running sandbox's addressExSandbox.Telemetry— the events both libraries emit, and their metadataExSandbox.Conformance.{Lifecycle, Isolation, ResourceLimits, Execution, Helpers, Group}— public by consequence:use ExSandbox.Conformanceexpands into calls on them inside the consumer's own module, so they are part of the compiled surface whether or not anyone intended it
Everything else is private
A module not listed above is private, whether or not it is namespaced
Internal (FR-014). The ExSandbox.Internal.* namespace makes the common
case obvious from the module name, but the list above is what defines the
boundary — a module that merely lacks the Internal prefix has not thereby
become public.
There is no compatibility promise for private modules. Calling one from a
consuming application is the coupling FR-004 forbids, and a consumer can
check for it mechanically rather than by review: this table also ships as
priv/boundary.md inside the package, resolvable at runtime through
Application.app_dir(:ex_sandbox, "priv/boundary.md"). A consumer's own test
reads the installed copy instead of restating it, so the check cannot drift
from the list it is checking against.
What a consumer must supply
| Consumer supplies | Why this library cannot |
|---|---|
owner_ref | It has no owner concept (FR-007) |
| Run policy | It has no lifecycle concept (FR-008) |
context value, or nil | It has no request-scoping type (FR-003) |
| Mechanism selection and configuration | The host decides what it can run |
A consumer supplying nothing beyond a mechanism gets a working sandbox.
Capability honesty
Every entry point below refuses to start a sandbox when a required capability
is unavailable, rather than starting it unconfined (FR-016; 005 R9). A
mechanism that cannot isolate must say so — reporting less than it does rather
than more is the discipline that makes a cross-platform floor mean anything.
Summary
Types
A module implementing ExSandbox.Mechanism.
Why an operation was refused before the mechanism was ever asked.
Functions
Reports on every capability this host provides.
Destroys a sandbox and releases its resources.
Runs {cmd, args} inside a running sandbox (008-FR-002, 007-FR-041).
Every sandbox the mechanism currently believes is running.
Creates the sandbox's resources without starting it.
Starts a provisioned sandbox.
The sandbox's current state, as the mechanism observes it.
Stops a running sandbox, leaving its resources in place.
Current resource consumption for one sandbox (003-FR-026).
Types
@type mechanism() :: module()
A module implementing ExSandbox.Mechanism.
@type refusal() :: {:capability_unavailable, [ExSandbox.Capability.t()]}
Why an operation was refused before the mechanism was ever asked.
{:capability_unavailable, reports} is not an error in the mechanism — it is
this library declining to pretend.
Functions
@spec capabilities() :: [ExSandbox.Capability.t()]
Reports on every capability this host provides.
Public so a consumer can decide before provisioning whether this host can run what they need, rather than discovering it from a refusal.
@spec destroy(mechanism(), ExSandbox.Sandbox.t()) :: :ok | {:error, term()}
Destroys a sandbox and releases its resources.
Deliberately not capability-gated. Refusing to clean up because the host cannot isolate would strand resources on exactly the hosts least able to afford them.
@spec execute( mechanism(), ExSandbox.Sandbox.t(), {String.t(), [String.t()]}, keyword() ) :: {:ok, ExSandbox.Mechanism.completion()} | {:error, {:could_not_run, term()}} | {:error, {:limit_exceeded, :wall_clock | :memory | :cpu}}
Runs {cmd, args} inside a running sandbox (008-FR-002, 007-FR-041).
Deliberately not capability-gated the way provision/2 and start/2 are,
and the reason is not laxity: this call reaches into a sandbox that is already
running, which means the capability decision was taken at its launch and taken
correctly, or there is no sandbox here to reach into. Re-asking now would only
add a second answer to a question already settled, and on a host whose report
changed mid-flight the second answer would refuse to read the output of work
that ran perfectly well under confinement that was real when it started.
The three returns are three different facts — see ExSandbox.Mechanism.execute/3.
In particular {:error, {:could_not_run, _}} is not an exit status.
Every sandbox the mechanism currently believes is running.
Nothing in the happy path calls this; it exists so a host can reconcile
recorded state against actual state after a restart (003-FR-015).
@spec provision(mechanism(), ExSandbox.Sandbox.t()) :: {:ok, ExSandbox.Sandbox.t()} | {:error, refusal() | term()}
Creates the sandbox's resources without starting it.
Refuses when the host lacks a capability the mechanism requires, rather than provisioning something that would run unconfined.
@spec start(mechanism(), ExSandbox.Sandbox.t()) :: {:ok, ExSandbox.Sandbox.t()} | {:error, refusal() | term()}
Starts a provisioned sandbox.
The capability check is repeated here rather than trusted from provision/2:
a sandbox may be provisioned on one host and started on another, and a cap
that was enforceable at provision time is not thereby enforceable now.
@spec status(mechanism(), ExSandbox.Sandbox.t()) :: {:ok, ExSandbox.Mechanism.status()} | {:error, term()}
The sandbox's current state, as the mechanism observes it.
@spec stop(mechanism(), ExSandbox.Sandbox.t()) :: {:ok, ExSandbox.Sandbox.t()} | {:error, term()}
Stops a running sandbox, leaving its resources in place.
@spec usage(mechanism(), ExSandbox.Sandbox.t()) :: {:ok, ExSandbox.Mechanism.usage()} | {:error, term()}
Current resource consumption for one sandbox (003-FR-026).