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

The behaviour every isolation mechanism implements (012 T019, T020).

A mechanism is the thing that actually runs code in isolation — a BEAM node
under OS confinement (`005-sandbox-beam`), a container (`001-lxd`), or
something a third party writes. `ExSandbox` dispatches to one; the conformance
suite holds all of them to the same bar.

Every callback takes an `ExSandbox.Sandbox.t()` — a plain struct — rather than
`003`'s `SandboxRecord.t()`, which was an Ash resource struct (research R3).
This is the change that would have been most expensive to defer: it alters the
type in every callback.

⚠️ **Do not trust a count of callbacks written in prose, here or anywhere
else.** This moduledoc said "seven" while the file declared eight, and
`012/contracts/execution-seam.md` re-titled its own heading for the same
reason. Count them in the file.

## Why more callbacks than the obvious four

The natural first cut of this API is `compile` / `start` / `stop` / `proxy`.
Several of the callbacks here exist for a specific reason rather than for
symmetry, and each would be easy to leave out:

  * `status` — `003-FR-024` requires distinguishing "starting" from "not
    running". `:absent` and `:unknown` must not collapse into each other:
    "we know it is gone" and "we cannot tell" lead to different actions.

  * `list_running` — **the one most easily dropped, because nothing in the
    happy path calls it.** It is what makes post-restart reconciliation
    possible at all (`003-FR-015`). Without it a sandbox that crashed while
    the host was down stays recorded as running indefinitely, and `003-SC-008`
    — recorded status matches reality within 60 seconds — is unsatisfiable by
    construction.

  * `usage` — per-sandbox CPU, memory, and disk, attributable to the owner
    (`003-FR-026`, `010` Story 3).

  * `execute` — **the seam that lets a caller run a command inside a sandbox
    that is already running**, added by `012/contracts/execution-seam.md`
    (decided 2026-08-20, Option A shape A2). Until it existed this behaviour
    could start a workload and stop it and never run anything beside it, so
    `008-FR-002` (verification runs inside a sandbox, never on the platform's
    runtime) and `007-FR-041` (a run does not get a private isolation
    mechanism separate from its environment's sandbox) were both unbuildable.
    The argument that every mechanism can keep this promise honestly is short:
    a mechanism that cannot run a command inside a sandbox cannot have started
    an application in one either.

## There is no `compile` callback

Building a tenant's application is **per-stack work**, owned by
`009-stack-adapters` and run *inside* an already-provisioned sandbox
(`007-FR-041`, `013-FR-021`). Putting it on the mechanism would require every
mechanism to know how to build every stack — precisely the coupling
Principle VI exists to prevent. A mechanism provisions a place to run things;
what gets built there is not its business.

## Optional: declaring required capabilities

A mechanism may export `required_capabilities/0` returning a list of
`ExSandbox.Capability.name()`. `ExSandbox` refuses to provision or start when
any of them is unavailable on this host. A mechanism that does **not** export
it is treated as requiring all of them — under-declaring must not be a way to
escape the check (`FR-012b`).

## Optional: declaring constructed capabilities

A mechanism may also export `constructed_capabilities/0`. ⚠️ **It is the
opposite claim from `required_capabilities/0`, and confusing the two inverts
the gate.** One says *what this mechanism needs the host to already provide*;
the other says *what this mechanism builds itself, whether or not the host
can*.

The distinction exists because the host probe answers a narrower question than
the gate asks. `ExSandbox.Capability` reports whether **this host** can confine
a process directly — on darwin every answer is reasoning about a BEAM node
under `sandbox-exec`. A mechanism whose isolation is a container satisfies the
same names by a different construction, and before this callback existed there
was no argument position in which to say so: the report was a host probe
wearing one mechanism's reasoning.

Omitting it reports nothing, so the gate is the host probe alone and every
mechanism written before this callback behaves exactly as it did.

# `completion`

```elixir
@type completion() :: %{
  exit_status: integer(),
  stdout: binary(),
  stderr: binary(),
  truncated?: boolean()
}
```

What one completed command produced.

`stdout` and `stderr` are **separate** because a merged stream cannot
attribute a failure, and `015` research R17 measured `MuonTrap`'s
`:logger_fun` corrupting lines past a 256-byte buffer — so how the two are
captured is a decision a mechanism must make deliberately, not a detail it may
inherit.

`truncated?` is explicit for the same reason: silent truncation of a build log
is how a real error disappears from a diagnosis. It is `true` when either
stream was cut, and the bytes returned are that stream's first bytes.

# `output_chunk`

```elixir
@type output_chunk() :: {:stdout | :stderr, binary()}
```

A chunk of output as it is produced, for `opts[:on_output]`.

⚠️ **A chunk is a chunk, not a line.** Nothing here promises line framing, and
a mechanism must not impose one: `015` R17's defect is precisely a line buffer
that corrupts what does not fit it. A caller wanting lines assembles them from
the byte stream, where a long line is late rather than mangled.

# `status`

```elixir
@type status() ::
  :absent
  | :provisioned
  | :starting
  | :running
  | :stopping
  | :stopped
  | :unknown
```

What a mechanism observes about a sandbox.

`:absent` ("it is definitely not there") and `:unknown` ("we could not
determine") are deliberately distinct — collapsing them loses the difference
between a sandbox that is gone and a mechanism that cannot see (`003-FR-024`).

# `usage`

```elixir
@type usage() :: %{
  optional(:cpu_millicores) =&gt; non_neg_integer(),
  optional(:memory_mb) =&gt; non_neg_integer(),
  optional(:disk_mb) =&gt; non_neg_integer()
}
```

Current consumption, for attribution to the owner (`003-FR-026`).

# `address`
*optional* 

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

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

Optional. A mechanism that omits it offers no reachability, which is what
every mechanism offered before this callback existed.

The address is a `host:port` string reachable **from the machine running the
platform** and from nowhere else. A mechanism that publishes on an interface
another machine can reach has not implemented this callback, it has exposed
the tenant's application: the two are indistinguishable to the caller, which
is why the constraint is stated here rather than left to each mechanism.

`{:ok, nil}` is the answer whenever there is no address to give -- the sandbox
names no `service_port`, it is not running, or this mechanism has no way to
publish one. It is deliberately not an error: "not reachable" is an ordinary
state of a sandbox, and a caller that has to rescue an error to render a
stopped sandbox will eventually render something else instead.

⚠️ A mechanism must not answer with a handle that merely identifies the
sandbox. `ExSandbox.Mechanism.Beam` returns `nil` for exactly this reason: it
has a `"peer:<id>"` reference, and a caller that put that in an `iframe` would
get a broken frame rather than a clear absence.

# `constructed_capabilities`
*optional* 

```elixir
@callback constructed_capabilities() :: [ExSandbox.Capability.name()]
```

Capabilities this mechanism constructs for whatever it runs.

Optional; a mechanism that omits it constructs nothing as far as the gate is
concerned, which is the safe direction and is exactly the pre-existing
behaviour.

⚠️ **Not the inverse-named twin of `required_capabilities/0` — the opposite
claim.** See the moduledoc. A mechanism that lists a name here is asserting
that a sandbox it starts is subject to that confinement even on a host whose
own probe reports it unavailable.

⚠️ **Nothing in this behaviour verifies the claim.** A mechanism that lists a
name it does not build has widened its own gate, and the compiler cannot tell.
`ExSandbox.Conformance` is what establishes such a claim, by observing a
breach being stopped rather than by confirming a limiter was invoked — the
discipline `005` R9b exists to enforce, where a cap was applied, silently lost
across an exec, and the process allocated 300 MB under a nominal 100 MB cap
and exited 0. Until a mechanism passes conformance, a name here is a promise
backed only by that mechanism's own tests.

# `destroy`

```elixir
@callback destroy(ExSandbox.Sandbox.t()) :: :ok | {:error, term()}
```

Destroys the sandbox and releases its resources.

Must be idempotent: a second destroy returns `:ok` rather than an error
(`003-FR-013`). A cleanup path that errors on "already gone" turns every
crash-recovery sweep into a source of spurious failures.

# `execute`

```elixir
@callback execute(
  ExSandbox.Sandbox.t(),
  {cmd :: String.t(), args :: [String.t()]},
  opts :: keyword()
) ::
  {:ok, completion()}
  | {:error, {:could_not_run, term()}}
  | {:error, {:limit_exceeded, :wall_clock | :memory | :cpu}}
```

Runs `{cmd, args}` inside a **running** sandbox and returns what it produced.

This is `012/contracts/execution-seam.md` Option A at shape A2: the call
returns on completion, and `opts[:on_output]` may supply a one-argument
function receiving `t:output_chunk/0` as output is produced. A caller passing
no sink gets exactly the run-to-completion shape, so nothing is paid for what
is not used, and `008-FR-061` / `007-FR-023` (in-progress visibility) do not
need a second breaking change to this behaviour later.

## The three returns are three different facts, and collapsing any two breaks
a requirement

  * `{:ok, completion}` — the command **ran**. `exit_status` is the command's
    own, whatever it is. A non-zero status is a *result*, not an error.
  * `{:error, {:could_not_run, reason}}` — the command **did not run**: the
    sandbox was gone, the binary was not there, the mechanism could not reach
    in. ⚠️ **This is not an exit status and must never be reported as one.**
    `008-FR-016` and `008-FR-026` both rest on it: an implementation that maps
    "the sandbox was gone" onto a non-zero exit has converted an *unperformed*
    check into a *failed* one, and a failed check consumes a refinement
    iteration that `FR-026` says it must not.
  * `{:error, {:limit_exceeded, capability}}` — the command was **stopped** by
    a limit the sandbox was launched under.

## Limits are the launch's, never this call's

A mechanism must not read a limit here and enforce it around the command.
`ExSandbox.Hardening`'s moduledoc records why: `005` R9b measured a cap
silently lost across an intervening exec, allocating 300 MB under a nominal
100 MB cap and **exiting 0**. A limit re-applied at execution time is a limit
applied after the process it governs already exists, which is the shape that
fails open. The sandbox is launched under the hardening layer's own command
construction -- `ExSandbox.Hardening.Linux.build_command/2` on Linux,
`ExSandbox.Hardening.Darwin.apply/3` on macOS -- and what runs inside it
inherits that confinement or the mechanism has none.

## Options

  * `:on_output` — `(t:output_chunk/0 -> any())`, optional.
  * `:timeout` — a wall-clock ceiling for this call, optional. A sandbox that
    declares its own budget is bounded by that budget regardless.

A mechanism may accept further options; it must not *require* any.

# `list_running`

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

Every `mechanism_ref` this mechanism currently believes is running.

Exists for reconciliation after a restart (`003-FR-015`). Nothing in the happy
path calls it, which is exactly why it is specified rather than left to each
mechanism to provide or not.

# `provision`

```elixir
@callback provision(ExSandbox.Sandbox.t()) ::
  {:ok, ExSandbox.Sandbox.t()} | {:error, term()}
```

Creates the sandbox's resources without starting it.

Returns the sandbox with `mechanism_ref` set — the opaque handle by which this
mechanism will recognise it later.

# `required_capabilities`
*optional* 

```elixir
@callback required_capabilities() :: [ExSandbox.Capability.name()]
```

Capabilities this mechanism requires of the host.

Optional. A mechanism that omits it is treated as requiring every known
capability — see the moduledoc.

# `start`

```elixir
@callback start(ExSandbox.Sandbox.t()) :: {:ok, ExSandbox.Sandbox.t()} | {:error, term()}
```

Starts a provisioned sandbox.

# `status`

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

The sandbox's current state as this mechanism observes it (`003-FR-024`).

# `stop`

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

Stops a running sandbox, leaving its resources intact.

# `usage`

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

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

---

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