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

What this library needs from its host, and whether it is actually there
(012 T013, T022, FR-016).

## Determined, never assumed

`FR-016` requires a library state what it needs and **report at runtime when
it is unavailable**, rather than assuming it. The distinction is not academic:
`005` R9 found that six of ten isolation criteria require Linux, and `013`
Finding V2 found a capability that is absent under some container
configurations. A library that assumed them would provide no isolation on
those hosts while claiming to.

## Availability is evidence, not configuration

A capability is reported available only on `FR-012a`'s evidence standard —
observed behaviour, not the presence of a mechanism. `005` R9b measured the
failure this guards against: `taskpolicy -m 100 sandbox-exec ... ./hog 300`
allocates 300 MB under a nominal 100 MB cap and exits 0, because the limit is
silently lost across the intervening `exec`. Every check short of *trigger a
breach and watch it stop* reports that composition as working.

So a check here answers "could this host enforce the cap at all", and it
errs toward `false`. Whether a **particular mechanism** actually enforces it
is `ExSandbox.Conformance`'s question, answered by breaching it.

## The coarse name and its decomposition (`014-FR-013a`, T004)

`:resource_limits` remains the **coarse, Linux-facing name**, and it stays.
ExSandbox.Mechanism.Beam's `required_capabilities/0` callback names it, and its comment
records why one name suffices there: the memory and CPU caps "both come from
the same cgroup scope, and a host with one has the other". On Linux that is
true, and a single gate over a single mechanism is the honest shape.

It stops being true off Linux, which is what `014-FR-013a` is about. `005`
R9b measured a macOS host where the caps come apart: `RLIMIT_CPU` is honoured
by the kernel while `RLIMIT_AS`/`DATA`/`RSS` fail `setrlimit` with `EINVAL`,
and the memory cap survives or is lost depending on where `taskpolicy` sits
in the process tree. One summary level cannot report that host without lying
in one direction or the other.

So `:process_separation`, `:memory_cap`, `:cpu_cap` and `:time_budget` are
`:resource_limits`' **per-capability decomposition** — an additional, finer
vocabulary, not a replacement. Two rules keep the two from drifting apart,
and both exist because a split verdict on one underlying fact is a defect
this file has already carried more than once (005 T060a5c, T060c):

  * On Linux the three host-enforced names are **derived** from the existing
    cgroup v2 probe rather than re-probing it. Two probes of one fact are two
    things that must stay equal forever, and the symptom of their drifting is
    a cap reported enforced on a host that does not enforce it.

  * `:time_budget` is derived from nothing, because it is **not a host fact
    at all**. `014-FR-014b` places its enforcement in the supervising BEAM,
    so no probe here can observe it and no cgroup verdict says anything about
    it. See `time_budget_not_a_host_capability/1`.

## Three Darwin names now report `available`, and what that cost (014 T020)

Every Darwin clause below used to report `unavailable`, on the correct
grounds that no backend existed to watch stopping a breach. `014` Phase 3
built one, and Phase 4 observed the breaches: `:memory_cap`, `:cpu_cap` and
`:process_separation` are derived from
`ExSandbox.Hardening.Darwin.capabilities/0`, whose probe runs the real
composition rather than looking for binaries on the `PATH`.

The claim rests on `ExSandbox.Hardening.DarwinOrderingTest` (`SC-003`), which
runs R9b's misordered composition and the backend's own composition in one
run and requires them to *differ* — 0 with 300 MB allocated against 137.
Without that pair, `FR-014a`'s standard is not met and these names go back.

Everything else on Darwin still reports `unavailable`, and each detail names
what is missing rather than merely asserting absence:

  * `:time_budget` — not a host fact anywhere, see
    `time_budget_not_a_host_capability/1`.
  * `:privilege_separation` — a deny-list `sandbox-exec` profile is not
    default-deny confinement (T021).
  * `:filesystem_confinement`, `:network_restriction`, `:disk_quota`,
    `:resource_limits` — each for the reason its clause states.

# `name`

```elixir
@type name() ::
  :resource_limits
  | :filesystem_confinement
  | :privilege_separation
  | :network_restriction
  | :disk_quota
  | :process_separation
  | :memory_cap
  | :cpu_cap
  | :time_budget
```

# `t`

```elixir
@type t() :: %ExSandbox.Capability{
  available?: boolean(),
  detail: String.t() | nil,
  name: name()
}
```

# `check`

```elixir
@spec check(name() | atom()) :: t()
```

Checks one capability against the running host.

Returns a report; it never raises, because "cannot determine" is a legitimate
answer that must be *reported* rather than thrown (`FR-012b`).

# `check_all`

```elixir
@spec check_all() :: [t()]
```

Checks every known capability.

# `gating_defaults`

```elixir
@spec gating_defaults() :: [name()]
```

The capabilities a mechanism is gated on when it declares none.

Deliberately narrower than `known/0` -- see the note above it. A name that is
`unavailable` on every host belongs in the report and never in the gate.

# `known`

```elixir
@spec known() :: [name()]
```

Every capability this library knows how to check.

# `missing`

```elixir
@spec missing([name()]) :: [t()]
```

The capabilities in `required` that this host cannot provide.

Returned rather than raised so a caller can report *which* one is missing —
"unavailable" with no detail is the kind of message that gets ignored.

# `satisfied?`

```elixir
@spec satisfied?([name()]) :: boolean()
```

True when every capability in `required` is available.

For a **host** deciding whether to offer a mechanism at all. A mechanism whose
required capability is missing must refuse rather than start unconfined
(spec Edge Cases; `005` R9's macOS rule), and this answers that in one call.

⚠️ This library's own gate is not this function. `ExSandbox.provision/2` and
`ExSandbox.start/2` go through a private `ensure_capable/2` built on
`missing/1`, because a refusal has to name *which* capability was absent and a
boolean cannot. This docstring said otherwise until 2026-08-29, which made a
reader looking for the gate look in the wrong place.

Not expressed as `missing(required) == []`, and the difference is measurable
rather than stylistic: `check/1` shells out for several names, and `Enum.all?`
stops at the first absent one where `missing/1` deliberately probes them all
so it can list them.

---

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