# `ExSandbox`
[🔗](https://github.com/FoundryStack/ex_sandbox/blob/v1.2.0/lib/ex_sandbox.ex#L1)

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 API
  * `ExSandbox.Mechanism` — the behaviour every isolation mechanism implements
  * `ExSandbox.Sandbox` — the struct passed to every mechanism callback
  * `ExSandbox.Capability` — the host capability report (`FR-016`)
  * `ExSandbox.Hardening` — the OS-level enforcement seam
  * `ExSandbox.Conformance` — the conformance suite, included via `use`
  * `ExSandbox.Proxy` — forwards a request to a running sandbox's address
  * `ExSandbox.Telemetry` — the events both libraries emit, and their metadata
  * `ExSandbox.Conformance.{Lifecycle, Isolation, ResourceLimits, Execution,
    Helpers, Group}` — public *by consequence*: `use ExSandbox.Conformance` expands
    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.

# `mechanism`

```elixir
@type mechanism() :: module()
```

A module implementing `ExSandbox.Mechanism`.

# `refusal`

```elixir
@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.

# `address`

```elixir
@spec address(mechanism(), ExSandbox.Sandbox.t()) ::
  {:ok, String.t() | nil} | {:error, term()}
```

Where an application inside the sandbox can be reached from this host, or
`nil`.

`{:ok, nil}` for a mechanism that does not implement the optional `address/1`
callback at all, which is the same answer one that implements it gives for a
sandbox with nothing to reach. A caller asking "where is this" gets one shape
back either way: making the mechanism's *choice to implement* visible here
would push a capability check into every call site, and each would have to
decide again what a missing callback means.

# `capabilities`

```elixir
@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.

# `destroy`

```elixir
@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.

# `execute`

```elixir
@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 `c:ExSandbox.Mechanism.execute/3`.
In particular `{:error, {:could_not_run, _}}` is **not** an exit status.

# `list_running`

```elixir
@spec list_running(mechanism()) :: {:ok, [String.t()]} | {:error, term()}
```

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`).

# `provision`

```elixir
@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.

# `start`

```elixir
@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.

# `status`

```elixir
@spec status(mechanism(), ExSandbox.Sandbox.t()) ::
  {:ok, ExSandbox.Mechanism.status()} | {:error, term()}
```

The sandbox's current state, as the mechanism observes it.

# `stop`

```elixir
@spec stop(mechanism(), ExSandbox.Sandbox.t()) ::
  {:ok, ExSandbox.Sandbox.t()} | {:error, term()}
```

Stops a running sandbox, leaving its resources in place.

# `usage`

```elixir
@spec usage(mechanism(), ExSandbox.Sandbox.t()) ::
  {:ok, ExSandbox.Mechanism.usage()} | {:error, term()}
```

Current resource consumption for one sandbox (`003-FR-026`).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
