# `Raxol.Core.Boundary.Path`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.7.0/lib/raxol/core/boundary/path.ex#L1)

Path-traversal confinement: resolve an untrusted `requested` reference under
a trusted `root` and prove the result stays inside that root, rejecting every
escape *before* any syscall touches the target.

This is the centralized seed of the "same gap, four patches" boundary
(PR #569 thread 2). It is one of **two** boundary confinements — the other is
`Raxol.Core.Boundary.TermText` (terminal-injection). They share a threat
narrative but nothing else, so they are two functions, not one.

Seeded from the proven `Raxol.AgentClientProtocol.Client.FsSandbox.resolve/2`
(leaf-symlink *and* symlinked-ancestor resolution, cycle-guarded, rejecting
before any target syscall) plus PA-6's ref-shape gate. The Agent Client Protocol package keeps
its own tested copy as a documented intentional duplicate; both bind to the
same shared conformance vectors (`test/support/boundary_vectors/`) so drift is
a red test, not a silent fork.

## Rule order

`confine/3` enforces these in order. Everything below the ref gate happens
before any syscall on the *target* (the only I/O is `:file.read_link` during
symlink resolution, which reads links, never opens/creates the target):

1. **Ref-shape gate** — when `ref_format:` is given, the RAW `requested`
   string must match the regex, else `{:error, :malformed_ref}`. Refs are
   validated as opaque tokens before they are ever treated as paths. Skipped
   when the option is absent.
2. **Lexical confinement** — `root = Path.expand(root)`;
   `lexical = Path.expand(Path.join(root, requested))`. Unless `lexical`
   equals `root` or starts with `root <> "/"`, `{:error, :path_traversal}`.
   (An absolute `requested` is jailed under `root` by `Path.join`, not
   honored.)
3. **Symlink resolution** — a hand-rolled realpath of BOTH `root` and
   `lexical`: follows a leaf symlink, recurses into the parent so a symlinked
   ancestor directory is caught too, resolves POSIX relative link targets
   against the link's own directory, and works for a not-yet-existing write
   leaf by resolving the deepest existing ancestor. Depth-capped at 40;
   a cycle yields `{:error, :too_many_symlinks}`.
4. **Post-resolution re-check** — the real path must still be within the real
   root, else `{:error, :symlink_escape}`.

Total function: never raises on bad input (a non-binary `root`/`requested`
yields `{:error, :invalid_input}`).

# `reason`

```elixir
@type reason() ::
  :malformed_ref
  | :path_traversal
  | :too_many_symlinks
  | :symlink_escape
  | :invalid_input
```

Rejection reason returned by `confine/3`.

# `confine`

```elixir
@spec confine(String.t(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, reason()}
```

Confine `requested` under `root`.

Returns `{:ok, real_absolute_path}` when the fully symlink-resolved target
provably stays inside the resolved root, or `{:error, reason}` (see the
moduledoc rule order for the exact reason per rejection).

## Options

  * `:ref_format` — a `Regex.t()` the RAW `requested` string must match before
    any resolution (e.g. PA-6's
    `~r/^(blobs|snapshots)\/[0-9a-f]{64}(\.json)?$/`). Absent = no gate.

---

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