# `Raxol.AgentClientProtocol.Ext.AttachPolicy.Bridge`
[🔗](https://github.com/DROOdotFOO/raxol-agent/blob/raxol_agent_client_protocol-v0.1.0/packages/raxol_agent_client_protocol/lib/raxol/agent_client_protocol/ext/attach_policy/bridge.ex#L1)

The BEAM-local `:authorize` bridge — make an in-process attach seam defer
to the SAME fail-closed admission funnel (`Ext.AttachPolicy.Runner`) the
untrusted ACP boundary uses.

## Why this exists (the two-wire boundary)

Admission is enforced at the process/trust boundary, NOT symmetrically on every
wire:

  * **ACP boundary (untrusted peers) — HARD-GATED.** `session/load` and
    `_raxol/session.load` converge on `Ext.Reattach.attach/1`, which runs the
    Runner funnel before any read. This is the security boundary.
  * **BEAM-local attach (in-process) — OPTIONAL SEAM.** An in-process caller
    can already read the session's journal store directly, so a gate on an
    in-process attach function is bypassable — it is a SEAM for a host
    that fronts reattach with its own transport, not a boundary.

This module is that seam's ready-made implementation. `authorizer/1` returns an
`authorize_fun` that translates the BEAM-local attach ctx into the Runner
ctx and returns the Runner's verdict verbatim. A host calls it before its own
in-process attach reads anything:

    authorize =
      Raxol.AgentClientProtocol.Ext.AttachPolicy.Bridge.authorizer(
        policy: MyPolicy,
        transport: %{kind: :process, peer: self()}
      )

    authorize.(%{session_id: session_id, from_offset: from_offset})

## Dependency direction (why it lives in THIS package)

This package has zero raxol-internal deps. The bridge references only THIS
package's Runner / Grant / policies and produces a PLAIN closure; the BEAM ctx
it receives is a duck-typed map. The host does the wiring.

## Fail-closed

The Runner returns `{:ok, %Grant{}}` (admit) or `{:denied, reason}` (deny). A
host admits only on `{:ok, _grant}` and reads nothing otherwise, so a denied
verdict cannot "mostly" admit. `transport` is Connection-sourced knowledge the
host MUST supply; absent (`nil`) the default `LocalNode` policy denies
(`:not_local` → `{:denied, :policy_error}`).

# `beam_ctx`

```elixir
@type beam_ctx() :: %{
  :session_id =&gt; String.t(),
  optional(:from_offset) =&gt; non_neg_integer(),
  optional(:surface) =&gt; atom(),
  optional(any()) =&gt; any()
}
```

The BEAM-local attach ctx a host builds
(`%{session_id, from_offset, policy, surface: :beam_local}`). Grow-only and
duck-typed — only `:session_id` is dereferenced hard; the rest default.

# `verdict`

```elixir
@type verdict() ::
  {:ok, Raxol.AgentClientProtocol.Ext.AttachPolicy.Grant.t()}
  | {:denied, atom()}
```

The Runner verdict, which is exactly the `authorize_fun` contract.

# `authorizer`

```elixir
@spec authorizer(keyword()) :: (beam_ctx() -&gt; verdict())
```

Build an `authorize_fun` for an in-process attach that defers to
`Ext.AttachPolicy.Runner`.

Options:

  * `:policy` — the `Ext.AttachPolicy` module (default
    `Ext.AttachPolicy.default_policy/0` = `LocalNode`, deny-by-default).
  * `:transport` — the Connection-sourced transport (`%{kind:, peer:}` | nil);
    NEVER peer-asserted. `LocalNode` denies a nil transport.
  * `:actor` — the audit actor map (`%{"id" => binary}`) | nil.
  * `:capability` — an `RXC1` capability token for the `Token` policy | nil.
  * `:task_supervisor`, `:timeout_ms` — passed through to `Runner.authorize/3`.

The returned fun reads the BEAM ctx's `:session_id`/`:from_offset`/`:surface`
and merges the config-supplied `:actor`/`:capability`/`:transport` into the
Runner ctx, then calls `Runner.authorize/3`. The BEAM ctx's `:policy`
(the HISTORY policy) is not an attach policy and is deliberately dropped —
admission is one decision gating both history and live.

---

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