# `ConduitMcp.Client.Policy.Paths`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/client/policy/paths.ex#L1)

Path canonicalization and path predicates for `ConduitMcp.Client.Policy`
argument rules (PO-9).

    root = "/srv/sandbox"

    ConduitMcp.Client.Policy.compile(
      deny: [{:args, ConduitMcp.Client.Policy.Paths.escapes("path", root)}],
      allow: [{:all, ["fs/write", {:args, ConduitMcp.Client.Policy.Paths.inside("path", root)}]}]
    )

Argument rules are **defence in depth, not a sandbox**: the server is the
enforcement point. These helpers only make the client's view of a path
hard to fool with encodings.

## Canonicalization

`canonicalize/3` turns an argument into an absolute, normalized path, or
fails:

  * the value must be a valid UTF-8 string with no NUL byte; it is
    normalized to Unicode NFC;
  * a relative path is joined to `root`; `~` is **not** expanded (it is an
    ordinary file name);
  * `.` segments and empty segments are dropped and `..` removes the
    previous segment, lexically (`..` at `/` stays at `/`);
  * with the `:resolve` option, the lexical result is passed to that
    function (for example one that resolves symlinks with
    `:file.read_link_all/1`) and its `{:ok, path}` result is normalized
    again; anything else is a failure. Nothing here touches the file
    system by itself.

The root goes through the same steps (it must be absolute), so
`inside?/3` compares like with like.

## Predicates

`inside/3` and `escapes/3` build `{:args, fun}` predicates that read one
argument, selected by a key (`"path"`) or a key/index list
(`["files", 0, "path"]`, string keys only). A list value is checked element
by element. They differ in how they fail:

  * `inside/3` is for allow rules: it matches only when every path
    canonicalizes and lies within the root. A missing argument, an empty
    list, or any failure is no match.
  * `escapes/3` is for deny and ask rules: it matches when any path fails
    to canonicalize (including a non-string value) or lies outside the
    root. A missing argument is no match (there is no path to check).

# `reason`

```elixir
@type reason() ::
  :not_a_string
  | :nul_byte
  | :invalid_unicode
  | :relative_root
  | :resolve_failed
```

Why a path did not canonicalize.

# `selector`

```elixir
@type selector() :: String.t() | [String.t() | non_neg_integer()]
```

Selects one argument: a key, or a list of keys and list indexes.

# `canonicalize`

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

Canonicalizes `path` against the absolute `root`; see the moduledoc.

## Options

  * `:resolve` - `(String.t() -> {:ok, String.t()} | {:error, term()})`,
    applied to the lexically normalized path (default: none).

## Examples

    iex> ConduitMcp.Client.Policy.Paths.canonicalize("../../etc/passwd", "/srv/sandbox")
    {:ok, "/etc/passwd"}

    iex> ConduitMcp.Client.Policy.Paths.canonicalize("a/./b//c/..", "/srv")
    {:ok, "/srv/a/b"}

    iex> ConduitMcp.Client.Policy.Paths.canonicalize("a\0b", "/srv")
    {:error, :nul_byte}

# `escapes`

```elixir
@spec escapes(selector(), String.t(), keyword()) :: (map() -&gt; boolean())
```

An argument predicate (for `{:args, fun}`) that matches when the selected
argument, or any element of it when it is a list, fails to canonicalize or
lies outside `root`.

Raises `ArgumentError` at build time when `root` is not an absolute path
or `selector` is malformed.

# `inside`

```elixir
@spec inside(selector(), String.t(), keyword()) :: (map() -&gt; boolean())
```

An argument predicate (for `{:args, fun}`) that matches when the selected
argument is a path, or a non-empty list of paths, all inside `root`.

Raises `ArgumentError` at build time when `root` is not an absolute path
or `selector` is malformed.

# `inside?`

```elixir
@spec inside?(term(), String.t(), keyword()) :: boolean()
```

Whether `path` canonicalizes and lies within `root` (or is `root` itself).
Takes the same options as `canonicalize/3`; any failure is `false`.

    iex> ConduitMcp.Client.Policy.Paths.inside?("/data/../etc", "/data")
    false
    iex> ConduitMcp.Client.Policy.Paths.inside?("/data/x", "/data")
    true

---

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