# `Nous.Sandbox.Seatbelt`
[🔗](https://github.com/nyo16/nous/blob/v0.17.1/lib/nous/sandbox/seatbelt.ex#L1)

macOS confinement via `sandbox-exec` and an inline SBPL profile.

`confine/2` wraps the caller's argv as

    /usr/bin/sandbox-exec -p <profile> -- <argv...>

The executable is the absolute `/usr/bin/sandbox-exec`, never a bare name, so
a poisoned `PATH` cannot substitute a different binary.

## The profile fences writes, not reads

The profile opens with `(allow default)`. That is deliberate and it is the
honest description of what this provider buys you: it denies every
`file-write*` **performed by the confined process**, then re-allows the
console `/dev` nodes and — under `:workspace_write` — every directory in
`Nous.Sandbox.writable_roots/1`. Reads, network, and process spawning are
**not** restricted. This is a write fence against a prompt-injected
`rm -rf`, not a confidentiality boundary.

`Nous.Sandbox.writable_roots/1` is the single owner of the writable set; this
module never recomputes it, so the profile cannot drift from the rest of the
system's idea of what is writable.

## Writes delegated to a running daemon are out of scope

"Denied" above is exactly as narrow as it reads: the *confined process* may
not perform the write. It does not mean no byte can land outside the writable
set. `(allow default)` permits `mach-lookup`, so the confined process can ask
an **already-running, unconfined** system daemon to write on its behalf, and
seatbelt checks the *daemon's* credentials, not ours. Executed, under the real
`:workspace_write` profile:

    sandbox-exec -p <profile> -- sh -c 'defaults write com.nous.esc pwned yes'

exits 0, prints no denial signature, and leaves a 56-byte plist in `$HOME` —
outside every entry of `writable_roots/1` — because `cfprefsd` performed the
write. Anything reachable over XPC/launchd that writes on a client's behalf
is the same class; where the Docker CLI is present, `docker run -v /:/host`
is that class with a much larger blast radius (reasoned, not executed).
Closing this would take a `deny mach-lookup (global-name ...)` list that
grows with every macOS release, so the boundary is named rather than patched.

Two things that look like this and are not: `osascript -e 'do shell script'`
(the process it spawns inherits our sandbox) and a nested `sandbox-exec`
(it cannot widen the profile — `sandbox_apply` fails with EPERM).

## The `/dev` write nodes are not a hole

`base_profile/0` re-allows `/dev/null`, `/dev/stdout`, `/dev/stderr`,
`/dev/tty` and `(subpath "/dev/fd")` in every mode, `:read_only` included.
Those are the caller's own already-open descriptors: the confined process can
write *through* an fd its unconfined parent opened, and it still cannot
`open()` a new path outside the writable set (verified under the real
`:workspace_write` profile — `exec 9> "$HOME/x"` is EPERM, and
`/dev/fd/<non-numeric>` is ENOENT, so nothing can be created there). Without
them `cmd > /dev/stdout`, `tee /dev/stderr`, `tee /dev/tty` and `> /dev/fd/1`
all failed with "Operation not permitted" — ordinary idioms in model-authored
shell — while all four succeed under `Nous.Sandbox.Bwrap`, whose `--dev /dev`
tmpfs makes those nodes writable. The two providers now tell the caller the
same story.

## On `sandbox-exec` being deprecated

Apple has marked `sandbox-exec` deprecated and has shipped no replacement for
sandboxing an arbitrary CLI process. It nevertheless still works on current
macOS and is what OpenAI's Codex CLI, Anthropic's Claude Code, and Chrome all
use for exactly this job. Depending on it is an accepted risk: if Apple
removes it, `Nous.Sandbox.chain/0` gains a replacement provider and nothing
above the seam changes. That is what the seam is for.

# `confine`

```elixir
@spec confine([String.t()], Nous.Sandbox.Policy.t()) ::
  {:ok, Nous.Sandbox.Confined.t()}
```

Wrap `argv` in `sandbox-exec` under an SBPL profile derived from `policy`.

Pure — it builds strings and returns data. The profile denies all writes
except the `/dev` console nodes and, under `:workspace_write`, the subpaths
reported by `Nous.Sandbox.writable_roots/1`.

# `probe`

```elixir
@spec probe(pos_integer()) :: {:ok, :full} | {:error, :unusable}
```

Check that `sandbox-exec` exists and that the kernel actually *enforces*
`base_profile/0`.

The probe runs the real `base_profile/0` — the same string `:read_only` gets,
which grants no writable root at all — and asserts both directions in one
spawn:

    /bin/sh -c 'echo x > /dev/null && ! : > "$0"' <path under System.tmp_dir!/0>

Exit 0 therefore means "a permitted write succeeded **and** a write outside
every writable root was actually refused". The previous probe ran
`/usr/bin/true` under `(version 1) (allow default)`, a profile with no deny
clause: it proved the binary exists and the parser was happy, never that one
write is stopped.

Exit 0 is `{:ok, :full}`; a missing binary, a nonzero exit, a timeout, or any
other runner error is `{:error, :unusable}`. Seatbelt has no partial grade —
either the kernel applies the profile or it does not — and anything ambiguous
fails closed.

---

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